Kotlin Collection Operations
Collections in Kotlin wouldn't be nearly as powerful without the rich set of operations the standard library provides. These operations make working with collections intuitive and efficient, enabling you to express complex data manipulations with minimal code.
Introduction to Collection Operations
Kotlin's collection operations are inspired by functional programming principles, allowing you to transform, filter, and manipulate collections without verbose iterative code. These operations can be chained together to create powerful data processing pipelines.
Most collection operations in Kotlin come in two flavors:
- Terminal operations - produce a single result (like
sum()
,count()
, orfind()
) - Intermediate operations - return another collection (like
map()
,filter()
, orsorted()
)
Let's explore the most essential collection operations that every Kotlin developer should know.
Basic Collection Operations
Accessing Elements
The most basic operations involve accessing collection elements:
val list = listOf("apple", "banana", "cherry")
// Accessing by index
val firstFruit = list[0] // "apple"
val lastFruit = list.last() // "cherry"
val secondFruit = list.getOrNull(1) // "banana"
val nonExistent = list.getOrNull(10) // null (safe access)
// Checking content
val hasBanana = "banana" in list // true
val hasOrange = list.contains("orange") // false
println("First fruit: $firstFruit")
println("Has orange? $hasOrange")
Output:
First fruit: apple
Has orange? false
Transforming Collections
map()
The map()
function transforms each element in a collection according to a given function:
val numbers = listOf(1, 2, 3, 4, 5)
val squared = numbers.map { it * it }
println("Original numbers: $numbers")
println("Squared numbers: $squared")
Output:
Original numbers: [1, 2, 3, 4, 5]
Squared numbers: [1, 4, 9, 16, 25]
flatMap()
flatMap()
transforms each element into a collection and then flattens the resulting collections into a single list:
val families = listOf(
listOf("John", "Jane", "Jack"),
listOf("Sarah", "Sam")
)
val allMembers = families.flatMap { it }
println("All family members: $allMembers")
Output:
All family members: [John, Jane, Jack, Sarah, Sam]
Filtering Collections
filter()
filter()
keeps only elements that satisfy a predicate:
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val evens = numbers.filter { it % 2 == 0 }
val odds = numbers.filter { it % 2 != 0 }
println("Even numbers: $evens")
println("Odd numbers: $odds")
Output:
Even numbers: [2, 4, 6, 8, 10]
Odd numbers: [1, 3, 5, 7, 9]
filterNot() and filterNotNull()
val mixed = listOf(1, 2, null, 4, null, 6)
val noNulls = mixed.filterNotNull()
val notEven = numbers.filterNot { it % 2 == 0 }
println("Without nulls: $noNulls")
println("Not even numbers: $notEven")
Output:
Without nulls: [1, 2, 4, 6]
Not even numbers: [1, 3, 5, 7, 9]
Aggregation Operations
Basic Aggregations
Kotlin provides convenient functions to aggregate collection elements:
val numbers = listOf(4, 2, 8, 6, 1, 9, 3)
println("Count: ${numbers.count()}")
println("Sum: ${numbers.sum()}")
println("Average: ${numbers.average()}")
println("Minimum: ${numbers.minOrNull()}")
println("Maximum: ${numbers.maxOrNull()}")
Output:
Count: 7
Sum: 33
Average: 4.714285714285714
Minimum: 1
Maximum: 9
reduce() and fold()
These operations accumulate values starting with a first element (reduce()
) or an initial value (fold()
):
val numbers = listOf(1, 2, 3, 4, 5)
// Reduce (no initial value)
val product = numbers.reduce { acc, next -> acc * next }
println("Product using reduce: $product") // 1*2*3*4*5 = 120
// Fold (with initial value)
val sum = numbers.fold(10) { acc, next -> acc + next }
println("Sum using fold with initial value 10: $sum") // 10+1+2+3+4+5 = 25
Output:
Product using reduce: 120
Sum using fold with initial value 10: 25
Collection Grouping and Partitioning
groupBy()
Groups elements based on a key derived from each element:
data class Person(val name: String, val age: Int)
val people = listOf(
Person("Alice", 31),
Person("Bob", 29),
Person("Charlie", 31),
Person("Diana", 25)
)
val byAge = people.groupBy { it.age }
println("Grouped by age:")
byAge.forEach { (age, peopleOfAge) ->
println("Age $age: ${peopleOfAge.map { it.name }}")
}
Output:
Grouped by age:
Age 31: [Alice, Charlie]
Age 29: [Bob]
Age 25: [Diana]
partition()
Splits a collection into two collections, based on a predicate:
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val (evens, odds) = numbers.partition { it % 2 == 0 }
println("Evens: $evens")
println("Odds: $odds")
Output:
Evens: [2, 4, 6, 8, 10]
Odds: [1, 3, 5, 7, 9]
Collection Ordering Operations
sorted() and sortedBy()
Sort elements in natural order or by a specific selector:
val unsorted = listOf(4, 2, 7, 5, 1, 6, 3)
val sorted = unsorted.sorted()
println("Unsorted: $unsorted")
println("Sorted: $sorted")
val people = listOf(
Person("Alice", 31),
Person("Bob", 29),
Person("Charlie", 31),
Person("Diana", 25)
)
val sortedByName = people.sortedBy { it.name }
val sortedByAge = people.sortedByDescending { it.age }
println("Sorted by name: ${sortedByName.map { it.name }}")
println("Sorted by age (descending): ${sortedByAge.map { "${it.name} (${it.age})" }}")
Output:
Unsorted: [4, 2, 7, 5, 1, 6, 3]
Sorted: [1, 2, 3, 4, 5, 6, 7]
Sorted by name: [Alice, Bob, Charlie, Diana]
Sorted by age (descending): [Alice (31), Charlie (31), Bob (29), Diana (25)]
Collection Search Operations
find() and firstOrNull()
Find the first element matching a predicate:
val people = listOf(
Person("Alice", 31),
Person("Bob", 29),
Person("Charlie", 31),
Person("Diana", 25)
)
val over30 = people.find { it.age > 30 }
val startsWithE = people.firstOrNull { it.name.startsWith("E") }
println("First person over 30: ${over30?.name ?: "None"}")
println("First person starting with E: ${startsWithE?.name ?: "None"}")
Output:
First person over 30: Alice
First person starting with E: None
any() and all()
Check if any or all elements satisfy a condition:
val numbers = listOf(2, 4, 6, 8, 9, 10)
val hasOdd = numbers.any { it % 2 != 0 }
val allEven = numbers.all { it % 2 == 0 }
val allLessThan20 = numbers.all { it < 20 }
println("Has odd numbers: $hasOdd")
println("All even numbers: $allEven")
println("All less than 20: $allLessThan20")
Output:
Has odd numbers: true
All even numbers: false
All less than 20: true
Real-World Examples
Filtering and Transforming Product Data
Let's imagine we have a list of products and want to extract information:
data class Product(
val id: String,
val name: String,
val price: Double,
val category: String,
val inStock: Boolean
)
val products = listOf(
Product("p1", "Laptop", 999.99, "Electronics", true),
Product("p2", "Headphones", 59.99, "Electronics", true),
Product("p3", "Notebook", 5.99, "Stationery", true),
Product("p4", "Pen", 1.99, "Stationery", true),
Product("p5", "Phone", 499.99, "Electronics", false),
Product("p6", "Desk", 199.99, "Furniture", true)
)
// Find available electronics, ordered by price
val availableElectronics = products
.filter { it.category == "Electronics" && it.inStock }
.sortedBy { it.price }
.map { "${it.name}: $${it.price}" }
println("Available electronics (ordered by price):")
availableElectronics.forEach { println("- $it") }
// Calculate average price per category
val avgPriceByCategory = products
.groupBy { it.category }
.mapValues { (_, productsInCategory) ->
productsInCategory.map { it.price }.average()
}
println("\nAverage price by category:")
avgPriceByCategory.forEach { (category, avgPrice) ->
println("$category: $${String.format("%.2f", avgPrice)}")
}
Output:
Available electronics (ordered by price):
- Headphones: $59.99
- Laptop: $999.99
Average price by category:
Electronics: $519.99
Stationery: $3.99
Furniture: $199.99
Processing User Activity Data
Imagine processing user activity logs:
data class ActivityLog(
val userId: Int,
val activity: String,
val durationMinutes: Int,
val date: String
)
val logs = listOf(
ActivityLog(1, "Login", 5, "2023-06-01"),
ActivityLog(2, "Search", 10, "2023-06-01"),
ActivityLog(1, "Upload", 15, "2023-06-01"),
ActivityLog(3, "Login", 3, "2023-06-01"),
ActivityLog(1, "Search", 8, "2023-06-02"),
ActivityLog(2, "Upload", 22, "2023-06-02"),
ActivityLog(3, "Search", 7, "2023-06-02")
)
// Total time spent per user
val timePerUser = logs
.groupBy { it.userId }
.mapValues { (_, userLogs) -> userLogs.sumOf { it.durationMinutes } }
println("Time spent per user (in minutes):")
timePerUser.forEach { (userId, totalTime) ->
println("User $userId: $totalTime minutes")
}
// Most common activity
val mostCommonActivity = logs
.groupingBy { it.activity }
.eachCount()
.maxByOrNull { it.value }
?.key
println("\nMost common activity: $mostCommonActivity")
// Average time per activity
val avgTimePerActivity = logs
.groupBy { it.activity }
.mapValues { (_, activityLogs) ->
activityLogs.map { it.durationMinutes }.average()
}
println("\nAverage time per activity (in minutes):")
avgTimePerActivity.forEach { (activity, avgTime) ->
println("$activity: ${String.format("%.1f", avgTime)} minutes")
}
Output:
Time spent per user (in minutes):
User 1: 28 minutes
User 2: 32 minutes
User 3: 10 minutes
Most common activity: Search
Average time per activity (in minutes):
Login: 4.0 minutes
Search: 8.3 minutes
Upload: 18.5 minutes
Combining Collection Operations
One of the most powerful aspects of Kotlin's collection operations is the ability to chain them together:
val result = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.filter { it % 2 == 0 } // Keep even numbers: [2, 4, 6, 8, 10]
.map { it * it } // Square them: [4, 16, 36, 64, 100]
.takeWhile { it < 50 } // Take until we reach 50: [4, 16, 36]
.sum() // Sum them up: 56
println("Result of combined operations: $result")
Output:
Result of combined operations: 56
Performance Considerations
When dealing with large collections, performance becomes important. Here are some tips:
- Use sequences for large collections: Sequences allow for lazy evaluation, processing one element at a time through the entire chain of operations instead of creating intermediate collections.
// Using regular collections (eager evaluation)
val result1 = (1..1000000)
.filter { it % 2 == 0 }
.map { it * it }
.take(5)
// Using sequences (lazy evaluation)
val result2 = (1..1000000)
.asSequence()
.filter { it % 2 == 0 }
.map { it * it }
.take(5)
.toList()
println("First 5 squared even numbers: $result2")
-
Choose the right operation: Some operations have optimized versions. For example, use
find()
instead offilter().first()
when you only need the first matching element. -
Minimize the number of passes: Try to combine multiple operations into a single pass. For example, use
mapNotNull()
instead ofmap().filterNotNull()
.
Summary
Kotlin's collection operations provide a powerful toolkit for working with collections. By using these operations, you can:
- Transform collections with
map()
andflatMap()
- Filter elements with
filter()
,filterNot()
, and related functions - Aggregate data using functions like
reduce()
,fold()
,sum()
, andaverage()
- Group and partition collections based on various criteria
- Search for elements that match specific conditions
- Sort collections in different ways
- Chain operations together for complex data processing tasks
These operations make your code more concise, readable, and expressive compared to traditional imperative approaches with loops and conditional statements.
Exercises
To reinforce your understanding of Kotlin collection operations, try these exercises:
-
Given a list of strings, create a function that returns a list containing only the strings that start with a vowel.
-
Given a list of people with names and ages, find the average age of people whose names start with each letter of the alphabet.
-
Write a function that takes a list of integers and returns a map where keys are the remainders when divided by 3, and values are lists of the original numbers.
-
Process a list of book objects (with title, author, and publication year) to find the most prolific author and the average publication year of their books.
-
Implement a function that takes a list of strings and returns a map with keys being the length of the string and values being the count of strings with that length.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)