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.
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:
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:
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.
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:
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:
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:
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:
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:
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
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
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()
andmapIndexed()
for one-to-one transformationsflatMap()
for one-to-many transformationszip()
for combining collectionsassociate()
for creating mapsgroupBy()
for categorizing datapartition()
for splitting collectionswindowed()
andchunked()
for working with blocks of elements
Exercises
To reinforce your understanding, try these exercises:
- Given a list of strings, create a new list that contains the length of each string.
- Transform a list of people into a map where the keys are their names and the values are their ages.
- Given a list of sentences, create a list of all the unique words across all sentences.
- Group a list of numbers based on whether they are prime or not.
- 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! :)