Chapter 8 of 12
~16 min read
Collections
Immutable vs mutable collections and the powerful Kotlin API that replaces Java Streams.
Immutable vs Mutable Collections
Kotlin makes immutability a conscious choice. listOf() returns a read-only list — use mutableListOf() when you need to modify it:
Java
// Mutable (default)
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
// "Immutable" via Collections.unmodifiable
List<String> fixed =
Collections.unmodifiableList(names);
// But still a runtime check, not compile-time Kotlin
// Read-only — no add/remove methods!
val names = listOf("Alice", "Bob")
// Mutable — has add/remove
val mutableNames = mutableListOf("Alice")
mutableNames.add("Bob")
mutableNames.remove("Alice")
// Map and Set:
val map = mapOf("a" to 1, "b" to 2)
val mutableMap = mutableMapOf("a" to 1)
mutableMap["c"] = 3 Read-only ≠ Immutable
A List<String> in Kotlin is a read-only view. The underlying collection may still be mutable. For true immutability, use persistentListOf() from the kotlinx.collections.immutable library.
Java Streams → Kotlin Collection API
Kotlin's collection functions are built-in — no .stream() needed:
Java Streams
List<String> names = List.of(
"Alice", "Bob", "Charlie", "Anna");
// Filter + map + collect
List<String> result = names.stream()
.filter(n -> n.startsWith("A"))
.map(String::toUpperCase)
.collect(Collectors.toList());
// [ALICE, ANNA] Kotlin
val names = listOf(
"Alice", "Bob", "Charlie", "Anna")
// No stream() needed — direct on collection
val result = names
.filter { it.startsWith("A") }
.map { it.uppercase() }
// [ALICE, ANNA]Common Collection Functions
Kotlin
data class User(val name: String, val age: Int, val city: String)
val users = listOf(
User("Alice", 30, "KL"),
User("Bob", 25, "Penang"),
User("Carol", 30, "KL"),
User("Dave", 22, "Penang")
)
// filter — keep matching
val adults = users.filter { it.age >= 25 }
// map — transform each element
val names = users.map { it.name }
// [Alice, Bob, Carol, Dave]
// find — first match or null
val kl = users.find { it.city == "KL" }
// groupBy — Map<Key, List<T>>
val byCity = users.groupBy { it.city }
// {KL=[Alice, Carol], Penang=[Bob, Dave]}
// sortedBy
val byAge = users.sortedBy { it.age }
// any / all / none
val hasMinors = users.any { it.age < 18 } // false
val allAdults = users.all { it.age >= 18 } // true
// reduce / fold
val totalAge = users.sumOf { it.age } // 107
// flatMap — flatten nested lists
val tags = listOf(
listOf("kotlin", "android"),
listOf("java", "spring")
)
val allTags = tags.flatMap { it }
// [kotlin, android, java, spring]Java vs Kotlin — API Quick Mapping
| Java Stream | Kotlin |
|---|---|
.filter(predicate) | .filter { } |
.map(Function) | .map { } |
.findFirst() | .find { } or .firstOrNull { } |
.sorted(Comparator) | .sortedBy { } / .sortedWith() |
.distinct() | .distinct() |
.collect(Collectors.groupingBy()) | .groupBy { } |
.flatMap(Function) | .flatMap { } |
.reduce(BinaryOperator) | .reduce { acc, it -> } |
.anyMatch(predicate) | .any { } |
.allMatch(predicate) | .all { } |
.count() | .count { } |
Sequences — Lazy Evaluation
Kotlin — use Sequence for large datasets
// Regular collection — eager (all items processed per step)
val result = bigList.filter { it.isActive }.map { it.name }
// Sequence — lazy (processes one element at a time)
val result = bigList.asSequence()
.filter { it.isActive }
.map { it.name }
.take(10)
.toList()
// More efficient for large lists with multiple operations