How do I combine multiple collection operations in a single Kotlin chain?

In Kotlin, you can combine multiple collection operations by chaining functions like filter, map, sortedBy, take, groupBy, and others.

Each operation returns a new collection, so you can call the next operation directly on the result.

val numbers = listOf(1, 2, 3, 4, 5, 6)

val result = numbers
    .filter { it % 2 == 0 }
    .map { it * 10 }
    .sorted()

println(result) // [20, 40, 60]

Here’s what happens:

  1. filter { it % 2 == 0 } keeps only even numbers
  2. map { it * 10 } transforms each number
  3. sorted() sorts the result

You can also chain operations on objects:

data class User(
    val name: String,
    val age: Int,
    val active: Boolean
)

val users = listOf(
    User("Alice", 30, true),
    User("Bob", 17, true),
    User("Charlie", 25, false),
    User("Diana", 22, true)
)

val activeAdultNames = users
    .filter { it.active }
    .filter { it.age >= 18 }
    .map { it.name }
    .sorted()

println(activeAdultNames) // [Alice, Diana]

You can often combine related filters into one:

val activeAdultNames = users
    .filter { it.active && it.age >= 18 }
    .map { it.name }
    .sorted()

For maps, you can chain over entries:

val scores = mapOf(
    "Alice" to 90,
    "Bob" to 75,
    "Charlie" to 85
)

val passedNames = scores
    .filter { (_, score) -> score >= 80 }
    .map { (name, _) -> name }
    .sorted()

println(passedNames) // [Alice, Charlie]

If the collection is large or the chain is expensive, use asSequence() to make intermediate operations lazy:

val result = numbers
    .asSequence()
    .filter { it % 2 == 0 }
    .map { it * 10 }
    .sorted()
    .toList()

Use regular collection chains for simple cases, and asSequence() when you want to avoid creating intermediate collections during multi-step processing.

How do I use destructuring with maps and pairs in Kotlin?

In Kotlin, destructuring lets you unpack values from an object into separate variables.

It is commonly used with:

  • Pair
  • Map.Entry
  • loops over maps
  • lambda parameters

Destructuring a Pair

A Pair<A, B> contains two values: first and second.

val pair = "Alice" to 25

val (name, age) = pair

println(name) // Alice
println(age)  // 25

This is equivalent to:

val name = pair.first
val age = pair.second

You can also create a Pair explicitly:

val pair = Pair("Kotlin", 2.1)

val (language, version) = pair

println(language) // Kotlin
println(version)  // 2.1

Destructuring map entries in a loop

When you iterate over a map, each item is a Map.Entry<K, V>. Kotlin lets you destructure it into key and value:

val ages = mapOf(
    "Alice" to 25,
    "Bob" to 30
)

for ((name, age) in ages) {
    println("$name is $age years old")
}

Here:

(name, age)

means:

name = entry.key
age = entry.value

Destructuring in lambdas

You can destructure map entries inside lambda parameters too:

val scores = mapOf(
    "Alice" to 90,
    "Bob" to 85,
    "Charlie" to 95
)

scores.forEach { (name, score) ->
    println("$name scored $score")
}

You can also use it with operations like map, filter, and sortedBy:

val passingStudents = scores.filter { (_, score) ->
    score >= 90
}

println(passingStudents) // {Alice=90, Charlie=95}

Ignoring values with _

If you only need one part of the destructured value, use _ for the part you want to ignore.

val user = "Alice" to 25

val (name, _) = user

println(name) // Alice

With a map:

val ages = mapOf(
    "Alice" to 25,
    "Bob" to 30
)

for ((name, _) in ages) {
    println(name)
}

Or if you only need values:

for ((_, age) in ages) {
    println(age)
}

Destructuring mutable maps

Destructuring works the same way with mutable maps:

val inventory = mutableMapOf(
    "Apples" to 10,
    "Oranges" to 5
)

for ((item, count) in inventory) {
    println("$item: $count")
}

If you want to update values, use the key:

for ((item, count) in inventory) {
    inventory[item] = count + 1
}

println(inventory) // {Apples=11, Oranges=6}

Important detail: destructuring depends on componentN() functions

Destructuring works when a type provides functions like:

component1()
component2()

Pair has:

component1() // first
component2() // second

Map entries support destructuring as:

component1() // key
component2() // value

So this:

val (key, value) = "id" to 123

is essentially:

val pair = "id" to 123
val key = pair.component1()
val value = pair.component2()

Quick summary

val pair = "Alice" to 25
val (name, age) = pair

val map = mapOf(
    "Alice" to 25,
    "Bob" to 30
)

for ((key, value) in map) {
    println("$key -> $value")
}

map.forEach { (key, value) ->
    println("$key -> $value")
}

Use destructuring when it makes key-value or pair-based code easier to read.

How do I use associateBy, partition, and zip in Kotlin?

In Kotlin, associateBy, partition, and zip are collection operations that help transform or split collections.

associateBy

associateBy creates a Map from a collection by choosing a key for each element.

data class User(val id: Int, val name: String)

val users = listOf(
    User(1, "Alice"),
    User(2, "Bob"),
    User(3, "Charlie")
)

val usersById = users.associateBy { user -> user.id }

println(usersById)

Output:

{
  1=User(id=1, name=Alice),
  2=User(id=2, name=Bob),
  3=User(id=3, name=Charlie)
}

You can also choose both the key and the value:

val namesById = users.associateBy(
    keySelector = { it.id },
    valueTransform = { it.name }
)

println(namesById)

Output:

{1=Alice, 2=Bob, 3=Charlie}

If multiple elements produce the same key, the last one wins:

val words = listOf("one", "two", "three", "ten")

val byFirstLetter = words.associateBy { it.first() }

println(byFirstLetter)

Output:

{o=one, t=ten}

Here, "ten" replaces "two" and "three" for key 't'.


partition

partition splits a collection into two lists based on a predicate.

It returns a Pair<List<T>, List<T>>:

  • first contains elements where the predicate is true
  • second contains elements where the predicate is false
val numbers = listOf(1, 2, 3, 4, 5, 6)

val result = numbers.partition { it % 2 == 0 }

val evens = result.first
val odds = result.second

println(evens)
println(odds)

Output:

[2, 4, 6]
[1, 3, 5]

Commonly, you destructure the result:

val (evens, odds) = numbers.partition { it % 2 == 0 }

println(evens)
println(odds)

Example with objects:

data class Task(val title: String, val done: Boolean)

val tasks = listOf(
    Task("Write tests", true),
    Task("Fix bug", false),
    Task("Deploy", false)
)

val (completed, pending) = tasks.partition { it.done }

println(completed)
println(pending)

zip

zip combines two collections element by element.

val names = listOf("Alice", "Bob", "Charlie")
val ages = listOf(25, 30, 35)

val people = names.zip(ages)

println(people)

Output:

[(Alice, 25), (Bob, 30), (Charlie, 35)]

The result is a List<Pair<A, B>>.

You can destructure each pair:

for ((name, age) in names.zip(ages)) {
    println("$name is $age years old")
}

Output:

Alice is 25 years old
Bob is 30 years old
Charlie is 35 years old

You can also provide a transform function:

val descriptions = names.zip(ages) { name, age ->
    "$name is $age years old"
}

println(descriptions)

Output:

[Alice is 25 years old, Bob is 30 years old, Charlie is 35 years old]

If the collections have different sizes, zip stops at the shorter one:

val letters = listOf("A", "B", "C")
val numbers = listOf(1, 2)

println(letters.zip(numbers))

Output:

[(A, 1), (B, 2)]

Quick comparison

Function Purpose Result
associateBy Turns a collection into a Map Map<K, T> or Map<K, V>
partition Splits a collection into two lists Pair<List<T>, List<T>>
zip Combines two collections element by element List<Pair<A, B>> or List<R>

Example using all three:

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

val people = listOf(
    Person(1, "Alice", 17),
    Person(2, "Bob", 21),
    Person(3, "Charlie", 30)
)

val peopleById = people.associateBy { it.id }

val (adults, minors) = people.partition { it.age >= 18 }

val namesWithAges = people.map { it.name }.zip(people.map { it.age })

println(peopleById)
println(adults)
println(minors)
println(namesWithAges)

Use:

  • associateBy when you want lookup by a key
  • partition when you want to split a list into matching and non-matching groups
  • zip when you want to pair two collections by position

How do I group and count elements using groupBy and count in Kotlin?

In Kotlin, you can group elements with groupBy, then count how many items are in each group.

Basic example

val words = listOf("apple", "banana", "apricot", "blueberry", "avocado")

val countsByFirstLetter = words
    .groupBy { it.first() }
    .mapValues { (_, words) -> words.count() }

println(countsByFirstLetter)

Output:

{a=3, b=2}

Here:

  • groupBy { it.first() } groups words by their first letter.
  • mapValues { it.value.count() } converts each grouped list into its size/count.

You can also write it as:

val countsByFirstLetter = words
    .groupBy { it.first() }
    .mapValues { it.value.size }

Counting objects by a property

data class Person(val name: String, val city: String)

val people = listOf(
    Person("Alice", "London"),
    Person("Bob", "Paris"),
    Person("Charlie", "London"),
    Person("Diana", "Paris"),
    Person("Eve", "Berlin")
)

val peopleByCity = people
    .groupBy { it.city }
    .mapValues { it.value.count() }

println(peopleByCity)

Output:

{London=2, Paris=2, Berlin=1}

More efficient option: groupingBy + eachCount

If you only need counts, prefer:

val countsByFirstLetter = words
    .groupingBy { it.first() }
    .eachCount()

This avoids creating intermediate lists for every group.

println(countsByFirstLetter)
// {a=3, b=2}

So the common choices are:

items.groupBy { key }.mapValues { it.value.count() }

or, more efficiently:

items.groupingBy { key }.eachCount()

How do I transform nested collections using flatMap in Kotlin?

In Kotlin, flatMap is used to transform each element into a collection and then flatten the results into a single list.

Basic idea

val result = items.flatMap { item ->
    // return a collection for each item
}

It is similar to:

items.map { ... }.flatten()

but more concise.


Example: flatten nested lists

val nested = listOf(
    listOf(1, 2),
    listOf(3, 4),
    listOf(5)
)

val flattened = nested.flatMap { it }

println(flattened)

Output:

[1, 2, 3, 4, 5]

Transform while flattening

val words = listOf("hi", "cat", "dog")

val letters = words.flatMap { word ->
    word.toList()
}

println(letters)

Output:

[h, i, c, a, t, d, o, g]

Each word becomes a list of characters, and flatMap combines them into one list.


Example with objects

data class Department(
    val name: String,
    val employees: List<String>
)

val departments = listOf(
    Department("Engineering", listOf("Alice", "Bob")),
    Department("HR", listOf("Carol"))
)

val allEmployees = departments.flatMap { department ->
    department.employees
}

println(allEmployees)

Output:

[Alice, Bob, Carol]

Transform nested values

data class Order(
    val id: Int,
    val items: List<String>
)

val orders = listOf(
    Order(1, listOf("Book", "Pen")),
    Order(2, listOf("Laptop"))
)

val itemDescriptions = orders.flatMap { order ->
    order.items.map { item ->
        "Order ${order.id}: $item"
    }
}

println(itemDescriptions)

Output:

[Order 1: Book, Order 1: Pen, Order 2: Laptop]

Here:

  1. Each Order is transformed into a List<String>
  2. flatMap flattens all those lists into one List<String>

map vs flatMap

Using map:

val result = orders.map { order ->
    order.items
}

Result type:

List<List<String>>

Using flatMap:

val result = orders.flatMap { order ->
    order.items
}

Result type:

List<String>

Rule of thumb

Use:

map

when each input becomes one output.

Use:

flatMap

when each input becomes many outputs, and you want a single flattened result.

val users = listOf(
    User("Alice", listOf("admin", "editor")),
    User("Bob", listOf("viewer"))
)

val roles = users.flatMap { it.roles }

Result:

[admin, editor, viewer]