Skip to main content

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(), or find())
  • Intermediate operations - return another collection (like map(), filter(), or sorted())

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:

kotlin
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:

kotlin
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:

kotlin
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:

kotlin
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()

kotlin
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:

kotlin
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()):

kotlin
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:

kotlin
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:

kotlin
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:

kotlin
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:

kotlin
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:

kotlin
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:

kotlin
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:

kotlin
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:

kotlin
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:

  1. 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.
kotlin
// 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")
  1. Choose the right operation: Some operations have optimized versions. For example, use find() instead of filter().first() when you only need the first matching element.

  2. Minimize the number of passes: Try to combine multiple operations into a single pass. For example, use mapNotNull() instead of map().filterNotNull().

Summary

Kotlin's collection operations provide a powerful toolkit for working with collections. By using these operations, you can:

  • Transform collections with map() and flatMap()
  • Filter elements with filter(), filterNot(), and related functions
  • Aggregate data using functions like reduce(), fold(), sum(), and average()
  • 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:

  1. Given a list of strings, create a function that returns a list containing only the strings that start with a vowel.

  2. Given a list of people with names and ages, find the average age of people whose names start with each letter of the alphabet.

  3. 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.

  4. 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.

  5. 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! :)