Skip to main content

Kotlin Collection Transformation

Collections are fundamental to most programming tasks, but their true power emerges when you can transform them to suit your needs. Kotlin provides an extensive set of functions specifically designed for collection transformation, allowing you to convert one collection into another with elegant, readable code.

Introduction to Collection Transformations

Collection transformations in Kotlin let you create new collections based on existing ones. Instead of manually iterating through elements and building new collections, Kotlin's built-in functions handle the boilerplate code, allowing you to focus on the transformation logic.

These functions are inspired by functional programming principles, making your code:

  • More concise and readable
  • Less prone to bugs
  • Easier to understand and maintain

Let's explore the most useful transformation functions that Kotlin offers.

Basic Transformations

The map() Function

The map() function applies a transformation to each element in a collection and returns a new collection with the transformed elements.

kotlin
val numbers = listOf(1, 2, 3, 4, 5)
val squared = numbers.map { it * it }
println(squared) // Output: [1, 4, 9, 16, 25]

In this example, we transform each number by squaring it. The original list remains unchanged.

Transforming Complex Objects

The real power of map() becomes evident when working with complex objects:

kotlin
data class Person(val name: String, val age: Int)

val people = listOf(
Person("Alice", 29),
Person("Bob", 31),
Person("Charlie", 25)
)

val names = people.map { it.name }
println(names) // Output: [Alice, Bob, Charlie]

val agesInFiveYears = people.map { it.name to (it.age + 5) }
println(agesInFiveYears) // Output: [(Alice, 34), (Bob, 36), (Charlie, 30)]

The mapIndexed() Function

Sometimes you need the index along with the element:

kotlin
val fruits = listOf("Apple", "Banana", "Cherry")
val indexedFruits = fruits.mapIndexed { index, fruit -> "$index: $fruit" }
println(indexedFruits) // Output: [0: Apple, 1: Banana, 2: Cherry]

Advanced Transformations

The flatMap() Function

flatMap() combines mapping and flattening operations. It applies a function to each element that returns a collection, and then flattens these collections into a single collection.

kotlin
val numbers = listOf(1, 2, 3)
val tripled = numbers.flatMap { listOf(it, it, it) }
println(tripled) // Output: [1, 1, 1, 2, 2, 2, 3, 3, 3]

// A more practical example:
data class Student(val name: String, val courses: List<String>)

val students = listOf(
Student("Alex", listOf("Math", "Physics")),
Student("Beth", listOf("Chemistry", "Biology", "Math"))
)

val allCourses = students.flatMap { it.courses }.distinct()
println(allCourses) // Output: [Math, Physics, Chemistry, Biology]

The zip() Function

zip() combines elements from two collections into pairs:

kotlin
val names = listOf("Alice", "Bob", "Charlie")
val ages = listOf(24, 31, 28)
val people = names.zip(ages)
println(people) // Output: [(Alice, 24), (Bob, 31), (Charlie, 28)]

// You can also transform the pairs:
val descriptions = names.zip(ages) { name, age -> "$name is $age years old" }
println(descriptions) // Output: [Alice is 24 years old, Bob is 31 years old, Charlie is 28 years old]

The associate() Function

associate() builds a map from collection elements:

kotlin
val names = listOf("Alice", "Bob", "Charlie")
val nameMap = names.associate { it to it.length }
println(nameMap) // Output: {Alice=5, Bob=3, Charlie=7}

// Using associateWith() and associateBy()
val lengthMap = names.associateWith { it.length }
println(lengthMap) // Output: {Alice=5, Bob=3, Charlie=7}

val reversedMap = names.associateBy { it.length }
println(reversedMap) // Output: {5=Alice, 3=Bob, 7=Charlie}

Grouping Transformations

The groupBy() Function

groupBy() splits a collection into groups based on a key:

kotlin
data class Person(val name: String, val city: String, val age: Int)

val people = listOf(
Person("Alice", "New York", 25),
Person("Bob", "Boston", 31),
Person("Charlie", "New York", 29),
Person("Diana", "Boston", 24)
)

val byCity = people.groupBy { it.city }
println(byCity.keys) // Output: [New York, Boston]
println(byCity["New York"]?.map { it.name }) // Output: [Alice, Charlie]

// Group by age range
val byAgeRange = people.groupBy { it.age / 10 * 10 } // Groups by decade
println(byAgeRange) // Output: {20=[Person(name=Alice, city=New York, age=25), Person(name=Diana, city=Boston, age=24)], 30=[Person(name=Bob, city=Boston, age=31), Person(name=Charlie, city=New York, age=29)]}

Partitioning Collections

The partition() Function

partition() splits a collection into two based on a predicate:

kotlin
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8)
val (evens, odds) = numbers.partition { it % 2 == 0 }
println(evens) // Output: [2, 4, 6, 8]
println(odds) // Output: [1, 3, 5, 7]

// A practical example:
val people = listOf(
Person("Alice", "New York", 25),
Person("Bob", "Boston", 31),
Person("Charlie", "New York", 29)
)

val (adults, minors) = people.partition { it.age >= 18 }
println("Adults: ${adults.size}, Minors: ${minors.size}")
// Output: Adults: 3, Minors: 0

Windowed Operations

The windowed() and chunked() Functions

These functions divide collections into smaller blocks:

kotlin
val numbers = (1..10).toList()

// Create windows of size 3, step by 1
val windows = numbers.windowed(size = 3, step = 1)
println(windows) // Output: [[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6], [5, 6, 7], [6, 7, 8], [7, 8, 9], [8, 9, 10]]

// Create windows of size 3, step by 2
val skippingWindows = numbers.windowed(size = 3, step = 2)
println(skippingWindows) // Output: [[1, 2, 3], [3, 4, 5], [5, 6, 7], [7, 8, 9]]

// Create chunks of size 3
val chunks = numbers.chunked(3)
println(chunks) // Output: [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]

// Transform each chunk
val sumOfChunks = numbers.chunked(3) { it.sum() }
println(sumOfChunks) // Output: [6, 15, 24, 10]

Real-World Application Examples

Processing User Data

kotlin
data class User(val id: Int, val name: String, val email: String, val active: Boolean)

val users = listOf(
User(1, "Alice", "[email protected]", true),
User(2, "Bob", "[email protected]", false),
User(3, "Charlie", "[email protected]", true),
User(4, "Diana", "[email protected]", true)
)

// Extract active user emails
val activeEmails = users
.filter { it.active }
.map { it.email }

println(activeEmails) // Output: [[email protected], [email protected], [email protected]]

// Group users by activity status
val usersByStatus = users.groupBy { it.active }
val activeUsers = usersByStatus[true] ?: emptyList()
val inactiveUsers = usersByStatus[false] ?: emptyList()

println("Active users: ${activeUsers.size}, Inactive users: ${inactiveUsers.size}")
// Output: Active users: 3, Inactive users: 1

Data Analysis on Sales Records

kotlin
data class Sale(val product: String, val amount: Double, val category: String)

val sales = listOf(
Sale("Laptop", 1200.0, "Electronics"),
Sale("T-shirt", 25.0, "Clothing"),
Sale("Monitor", 350.0, "Electronics"),
Sale("Jeans", 45.0, "Clothing"),
Sale("Phone", 800.0, "Electronics")
)

// Calculate total sales by category
val salesByCategory = sales
.groupBy { it.category }
.mapValues { (_, sales) -> sales.sumOf { it.amount } }

println(salesByCategory)
// Output: {Electronics=2350.0, Clothing=70.0}

// Find the highest-priced item in each category
val highestPriceByCategory = sales
.groupBy { it.category }
.mapValues { (_, categorySales) ->
categorySales.maxByOrNull { it.amount }?.product
}

println(highestPriceByCategory)
// Output: {Electronics=Laptop, Clothing=Jeans}

Summary

Kotlin collection transformations provide powerful tools for working with data. Rather than writing imperative loops, you can express your intent in a more declarative, functional style. This leads to code that is:

  • More concise and expressive
  • Less prone to bugs through immutability
  • Easier to read and maintain
  • More composable through function chaining

The key functions we've covered include:

  • map() and mapIndexed() for one-to-one transformations
  • flatMap() for one-to-many transformations
  • zip() for combining collections
  • associate() for creating maps
  • groupBy() for categorizing data
  • partition() for splitting collections
  • windowed() and chunked() for working with blocks of elements

Exercises

To reinforce your understanding, try these exercises:

  1. Given a list of strings, create a new list that contains the length of each string.
  2. Transform a list of people into a map where the keys are their names and the values are their ages.
  3. Given a list of sentences, create a list of all the unique words across all sentences.
  4. Group a list of numbers based on whether they are prime or not.
  5. For a list of transactions with dates, group them by month and calculate the total amount for each month.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)