How do I use sequences to optimize performance with large Kotlin collections?

In Kotlin, sequences let you process large collections lazily, which can reduce temporary allocations and improve performance for chained operations.

The problem with regular collections

Collection operations like map, filter, and flatMap are usually eager:

val result = users
    .filter { it.isActive }
    .map { it.email }
    .take(10)

With a List, Kotlin typically creates intermediate collections:

  1. one list after filter
  2. another list after map
  3. then takes 10 items

For small collections, this is fine. For large collections, it can waste memory and CPU.

Use asSequence()

Convert the collection to a sequence:

val result = users
    .asSequence()
    .filter { it.isActive }
    .map { it.email }
    .take(10)
    .toList()

Now each element flows through the pipeline one at a time:

user -> filter -> map -> maybe included in result

Also, because of take(10), processing can stop as soon as 10 matching items are found.

Example

data class User(
    val name: String,
    val email: String,
    val isActive: Boolean
)

val emails = users
    .asSequence()
    .filter { it.isActive }
    .map { it.email.lowercase() }
    .distinct()
    .take(100)
    .toList()

This avoids building large intermediate lists before getting the final 100 emails.

When sequences help

Sequences are usually useful when you have:

  • large collections
  • multiple chained operations
  • short-circuiting operations, such as:
    • take
    • first
    • firstOrNull
    • any
    • all
    • none
    • find

Example:

val firstAdminEmail = users
    .asSequence()
    .filter { it.isActive }
    .filter { it.role == "admin" }
    .map { it.email }
    .firstOrNull()

This stops as soon as the first matching user is found.

When sequences may not help

Sequences are not always faster. They can be slower for:

  • small collections
  • simple one-step operations
  • cases where you need the entire result anyway
  • performance-critical tight loops where sequence overhead matters

For example, this may not benefit much:

val names = users.map { it.name }

A plain collection operation is often fine here.

Common pattern

Use this pattern:

val result = largeList
    .asSequence()
    .filter { condition(it) }
    .map { transform(it) }
    .take(50)
    .toList()

The important parts are:

  • asSequence() starts lazy processing
  • intermediate operations stay lazy
  • terminal operations execute the pipeline

Terminal operations include:

toList()
count()
first()
firstOrNull()
sumOf { ... }
any { ... }
forEach { ... }

Rule of thumb

Use sequences when your pipeline is large, chained, and potentially short-circuited.

For small or simple transformations, prefer normal collection operations for readability and often better performance.

How do I use immutability and copy-on-write with Kotlin collections?

In Kotlin, read-only collection interfaces plus copy-on-write updates are a common way to model immutable-style data.

Kotlin has two main collection interface families:

Mutable Read-only
MutableList<T> List<T>
MutableSet<T> Set<T>
MutableMap<K, V> Map<K, V>

Read-only interfaces prevent mutation through that reference, while copy-on-write means you create a new collection when you need a changed version.

Use read-only collection types

Prefer exposing List, Set, and Map instead of mutable versions:

class UserGroup(
    val users: List<String>
)

val group = UserGroup(listOf("Alice", "Bob"))

// group.users.add("Charlie") // Does not compile

This prevents callers from mutating the collection directly.

Copy-on-write with lists

To “modify” a list, create a new one:

val users = listOf("Alice", "Bob")

val withCharlie = users + "Charlie"
val withoutAlice = users - "Alice"

println(users)        // [Alice, Bob]
println(withCharlie)  // [Alice, Bob, Charlie]
println(withoutAlice) // [Bob]

The original list remains unchanged.

Copy-on-write with sets

val roles = setOf("USER", "ADMIN")

val updatedRoles = roles + "AUDITOR"
val reducedRoles = roles - "ADMIN"

println(roles)        // [USER, ADMIN]
println(updatedRoles) // [USER, ADMIN, AUDITOR]
println(reducedRoles) // [USER]

Copy-on-write with maps

val settings = mapOf(
    "theme" to "dark",
    "language" to "en"
)

val updatedSettings = settings + ("theme" to "light")
val removedSetting = settings - "language"

println(settings)
// {theme=dark, language=en}

println(updatedSettings)
// {theme=light, language=en}

println(removedSetting)
// {theme=dark}

Updating objects with immutable collections

This pattern is especially useful with data class and copy():

data class Team(
    val name: String,
    val members: List<String>
)

val team = Team(
    name = "Backend",
    members = listOf("Alice", "Bob")
)

val updatedTeam = team.copy(
    members = team.members + "Charlie"
)

println(team)
// Team(name=Backend, members=[Alice, Bob])

println(updatedTeam)
// Team(name=Backend, members=[Alice, Bob, Charlie])

Avoid leaking mutable collections

Be careful with this:

val mutableUsers = mutableListOf("Alice", "Bob")
val users: List<String> = mutableUsers

mutableUsers += "Charlie"

println(users) // [Alice, Bob, Charlie]

Even though users is typed as List<String>, the underlying collection is still mutable.

If you need a defensive copy:

val users: List<String> = mutableUsers.toList()

Encapsulate mutation internally

A common pattern is to keep a mutable collection private, but expose a read-only copy:

class UserRegistry {
    private val users = mutableListOf<String>()

    fun addUser(user: String) {
        users += user
    }

    fun users(): List<String> = users.toList()
}

This prevents external code from modifying your internal list.

Prefer immutable state updates

For state management, prefer this style:

data class AppState(
    val selectedIds: Set<Int>,
    val usersById: Map<Int, String>
)

val state = AppState(
    selectedIds = setOf(1, 2),
    usersById = mapOf(
        1 to "Alice",
        2 to "Bob"
    )
)

val newState = state.copy(
    selectedIds = state.selectedIds + 3,
    usersById = state.usersById + (3 to "Charlie")
)

Use persistent collections for heavy updates

Kotlin’s standard collections are read-only interfaces, but not deeply immutable persistent data structures.

If you frequently create modified copies of large collections, consider kotlinx.collections.immutable:

import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf

val users: PersistentList<String> = persistentListOf("Alice", "Bob")

val updatedUsers = users.add("Charlie")

println(users)        // [Alice, Bob]
println(updatedUsers) // [Alice, Bob, Charlie]

Persistent collections share internal structure, so copy-on-write-style updates are usually more efficient than copying a whole List.

Summary

Use:

val names: List<String> = listOf("Alice", "Bob")
val updated = names + "Charlie"

Instead of:

val names = mutableListOf("Alice", "Bob")
names += "Charlie"

General guidance:

  • Expose List, Set, and Map, not mutable types.
  • Use +, -, map, filter, and copy() to create updated values.
  • Avoid leaking mutable backing collections.
  • Use toList(), toSet(), or toMap() for defensive copies.
  • Use kotlinx.collections.immutable for efficient persistent immutable collections.

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]

How do I use map, filter and foreach with Kotlin collections?

In Kotlin collections:

  • map transforms each element into a new value.
  • filter keeps only elements that match a condition.
  • forEach performs an action for each element.

map: transform elements

Use map when you want to create a new collection by changing each item.

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

val doubled = numbers.map { number ->
    number * 2
}

println(doubled) // [2, 4, 6, 8]

You can use it when the lambda has one parameter:

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

val doubled = numbers.map { it * 2 }

println(doubled) // [2, 4, 6, 8]

filter: keep matching elements

Use filter when you want only items that satisfy a condition.

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

val evenNumbers = numbers.filter { it % 2 == 0 }

println(evenNumbers) // [2, 4, 6]

Another example with strings:

val names = listOf("Alice", "Bob", "Charlie", "Anna")

val namesStartingWithA = names.filter { it.startsWith("A") }

println(namesStartingWithA) // [Alice, Anna]

forEach: perform an action

Use forEach when you want to do something with each element, such as printing.

val names = listOf("Alice", "Bob", "Charlie")

names.forEach { name ->
    println(name)
}

Using it:

val names = listOf("Alice", "Bob", "Charlie")

names.forEach {
    println(it)
}

Chaining them together

You can combine filter, map, and forEach.

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

numbers
    .filter { it % 2 == 0 }
    .map { it * 10 }
    .forEach { println(it) }

Output:

20
40
60

This means:

  1. Keep only even numbers: [2, 4, 6]
  2. Multiply each by 10: [20, 40, 60]
  3. Print each result

Example with objects

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

val users = listOf(
    User("Alice", 25),
    User("Bob", 17),
    User("Charlie", 30)
)

val adultNames = users
    .filter { it.age >= 18 }
    .map { it.name }

println(adultNames) // [Alice, Charlie]

Important difference

map and filter return new collections:

val numbers = listOf(1, 2, 3)

val doubled = numbers.map { it * 2 }

println(numbers) // [1, 2, 3]
println(doubled) // [2, 4, 6]

forEach is usually used for side effects and does not create a transformed list:

val numbers = listOf(1, 2, 3)

numbers.forEach { println(it) }

Quick summary

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

val squared = numbers.map { it * it }
// [1, 4, 9, 16, 25]

val greaterThanTwo = numbers.filter { it > 2 }
// [3, 4, 5]

numbers.forEach { println(it) }
// Prints each number

Use:

  • map when you want to transform values
  • filter when you want to select values
  • forEach when you want to perform an action for each value

How do I convert arrays to lists and vice versa in Kotlin?

In Kotlin, you can convert arrays to lists and lists to arrays using standard library functions.

Array to List

Use toList():

val array = arrayOf("a", "b", "c")

val list: List<String> = array.toList()

println(list) // [a, b, c]

If you want a mutable list, use toMutableList():

val array = arrayOf("a", "b", "c")

val mutableList: MutableList<String> = array.toMutableList()

mutableList.add("d")

println(mutableList) // [a, b, c, d]

List to Array

Use toTypedArray():

val list = listOf("a", "b", "c")

val array: Array<String> = list.toTypedArray()

println(array.contentToString()) // [a, b, c]

Primitive Arrays

Kotlin has special primitive array types like IntArray, DoubleArray, and BooleanArray.

IntArray to List

val intArray = intArrayOf(1, 2, 3)

val list: List<Int> = intArray.toList()

println(list) // [1, 2, 3]

List<Int> to IntArray

Use toIntArray():

val list = listOf(1, 2, 3)

val intArray: IntArray = list.toIntArray()

println(intArray.contentToString()) // [1, 2, 3]

Other primitive conversions work similarly:

val doubles: List<Double> = listOf(1.1, 2.2, 3.3)
val doubleArray: DoubleArray = doubles.toDoubleArray()

val booleans: List<Boolean> = listOf(true, false)
val booleanArray: BooleanArray = booleans.toBooleanArray()

Important Note: asList()

For object arrays, you can also use asList():

val array = arrayOf("a", "b", "c")

val list = array.asList()

The difference is:

val array = arrayOf("a", "b", "c")

val copiedList = array.toList()
val backedList = array.asList()
  • toList() creates a new list copy.
  • asList() returns a list backed by the original array.

Example:

val array = arrayOf("a", "b", "c")
val list = array.asList()

array[0] = "z"

println(list) // [z, b, c]

So in most cases, use:

array.toList()
list.toTypedArray()

And for primitive types:

intArray.toList()
list.toIntArray()

How do I sort a list of values in Kotlin?

In Kotlin, you usually sort a list with sorted():

val numbers = listOf(5, 2, 8, 1)

val sortedNumbers = numbers.sorted()

println(sortedNumbers) // [1, 2, 5, 8]

sorted() returns a new sorted list and does not change the original list.

For descending order, use sortedDescending():

val numbers = listOf(5, 2, 8, 1)

val sortedDescending = numbers.sortedDescending()

println(sortedDescending) // [8, 5, 2, 1]

For objects, sort by a property with sortedBy():

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

val people = listOf(
    Person("Alice", 30),
    Person("Bob", 25),
    Person("Charlie", 35)
)

val sortedByAge = people.sortedBy { it.age }

println(sortedByAge)
// [Person(name=Bob, age=25), Person(name=Alice, age=30), Person(name=Charlie, age=35)]

If you have a mutable list and want to sort it in place, use sort():

val numbers = mutableListOf(5, 2, 8, 1)

numbers.sort()

println(numbers) // [1, 2, 5, 8]

Quick summary:

  • sorted() — returns a new ascending list
  • sortedDescending() — returns a new descending list
  • sortedBy { ... } — sorts by a property
  • sort() — sorts a mutable list in place