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