Collections and Sequences
|
This section documents Kotlin 2.4.x on the JVM, as published at kotlinlang.org, which is the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against kotlinlang.org before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Kotlin’s collection interfaces sit directly on top of java.util.Collection/List/Set/Map, adding a
read-only/mutable split at the interface level and a large functional API on top.
Read-Only vs. Mutable Collection Interfaces
Kotlin exposes every collection through two interface families: List/Set/Map expose only read
operations, while MutableList/MutableSet/MutableMap add add/remove/put/etc. There is no separate
runtime type behind this — it is purely a compile-time view; the concrete class underneath (ArrayList,
LinkedHashMap, …) is exactly Java’s:
val readOnly: List<Int> = listOf(1, 2, 3) // no add()/remove() visible through this reference
val mutable: MutableList<Int> = mutableListOf(1, 2, 3)
mutable.add(4)
// readOnly.add(4) // compile error: List has no "add"
val alsoMutable = readOnly as? MutableList<Int> // a *view* cast is still possible if the underlying
alsoMutable?.add(4) // object really is mutable -- this is a design smell, not a guarantee
This is a compile-time safety net, not an immutability guarantee the way List.of(…) is closer to in modern
Java (which returns a genuinely immutable implementation) — a Kotlin List reference can still be backed by a
mutable list that some other, MutableList-typed reference to the same object changes underneath it.
Collection Builders
listOf/setOf/mapOf (read-only) and mutableListOf/mutableSetOf/mutableMapOf (mutable) are the
standard factory functions, alongside buildList { }/buildMap { } for building up a collection imperatively
and exposing it as read-only afterward:
val names = listOf("Ada", "Grace", "Bo")
val scores = mapOf("Ada" to 95, "Grace" to 98)
val computed = buildList {
add("first")
if (scores.isNotEmpty()) add("has scores")
}
Functional Operations
The bulk of everyday collection code uses a chain of functional operations rather than manual loops — Kotlin’s answer to Java’s Stream API, but operating directly on the collection interfaces themselves rather
than through a separate stream type:
val numbers = listOf(1, 2, 3, 4, 5, 6)
val doubled = numbers.map { it * 2 } // [2, 4, 6, 8, 10, 12]
val evens = numbers.filter { it % 2 == 0 } // [2, 4, 6]
val total = numbers.fold(0) { acc, n -> acc + n } // 21 -- like Stream.reduce with a seed
val byParity = numbers.groupBy { if (it % 2 == 0) "even" else "odd" }
// {odd=[1, 3, 5], even=[2, 4, 6]}
data class Person(val id: Int, val name: String)
val people = listOf(Person(1, "Ada"), Person(2, "Grace"))
val byId = people.associateBy { it.id } // Map<Int, Person>: {1=Person(...), 2=Person(...)}
map/filter/fold/groupBy/associateBy are all defined directly as extension functions on Iterable<T>
(see Extension Functions and Scope
Functions), so they work uniformly across List, Set, and any custom Iterable.
Sequence (Lazy) vs. Eager Collections
Every operation above, called on a List/Set, runs eagerly: each .map { }/.filter { } in a chain
allocates and fully populates a new intermediate list before the next step runs. Sequence runs the same
operations lazily, element by element, exactly like a Java Stream:
val eager = listOf(1, 2, 3, 4, 5)
.map { println("map $it"); it * 2 } // fully evaluated -- prints 5 times right here
.first { it > 4 }
val lazy = sequenceOf(1, 2, 3, 4, 5)
.map { println("map $it"); it * 2 } // nothing runs yet
.first { it > 4 } // pulls elements one at a time until satisfied,
// so "map" only prints for 1, 2, 3 -- not all 5
.asSequence() converts any Iterable to a Sequence. The difference matters for large or infinite
collections and for short-circuiting chains (.first { }, .take(n), .any { }): a Sequence avoids
building full intermediate collections at every step, at the cost of some per-element overhead that makes eager
collections faster for small, fully-consumed chains. As a rule of thumb: default to List, and reach for
Sequence specifically when a chain is long, the source is large, or the terminal operation can stop early.
See Also
-
Generics and Variance — why
List<out T>is covariant butMutableList<T>is not. -
Extension Functions and Scope Functions — how
map/filter/etc. are themselves just extension functions. -
Flows — `Sequence’s asynchronous counterpart for values arriving over time.
References
-
Kotlin docs — Collection operations overview (including
map/filter/fold/groupBy/associateBy).