07 · Collections¶
Kotlin's standard library ships with rich, well-designed collection types out
of the box — List, Set, and Map — each with both read-only and mutable
variants. This module covers creating and using them; the powerful
functional operations (map, filter, reduce) get their own deep dive in
Level 2.
Lists¶
A List is an ordered collection that allows duplicates. listOf(...)
creates a read-only list; mutableListOf(...) creates one you can modify.
fun main() {
val fruits: List<String> = listOf("apple", "banana", "cherry")
println(fruits) // [apple, banana, cherry]
println(fruits[0]) // apple
println(fruits.size) // 3
// fruits.add("date") // compile error -- List has no add()
val mutableFruits: MutableList<String> = mutableListOf("apple", "banana")
mutableFruits.add("cherry")
mutableFruits.remove("apple")
println(mutableFruits) // [banana, cherry]
}
Note that List is read-only, not necessarily immutable — it simply doesn't
expose mutating methods. It's a view; whether the underlying data can change
elsewhere depends on what created it.
Common list operations¶
fun main() {
val numbers = listOf(5, 3, 8, 1, 9, 3)
println(numbers.first()) // 5
println(numbers.last()) // 3
println(numbers.contains(8)) // true
println(numbers.indexOf(8)) // 2
println(numbers.sorted()) // [1, 3, 3, 5, 8, 9]
println(numbers.reversed()) // [3, 9, 1, 8, 3, 5]
println(numbers.max()) // 9
println(numbers.min()) // 1
println(numbers.sum()) // 29
println(numbers.distinct()) // [5, 3, 8, 1, 9]
}
Sets¶
A Set is an unordered collection with no duplicate elements.
fun main() {
val uniqueNumbers: Set<Int> = setOf(1, 2, 2, 3, 3, 3)
println(uniqueNumbers) // [1, 2, 3] -- duplicates collapsed
val mutableSet: MutableSet<String> = mutableSetOf("a", "b")
mutableSet.add("c")
mutableSet.add("a") // no-op -- already present
println(mutableSet) // [a, b, c]
val setA = setOf(1, 2, 3)
val setB = setOf(2, 3, 4)
println(setA.union(setB)) // [1, 2, 3, 4]
println(setA.intersect(setB)) // [2, 3]
println(setA.subtract(setB)) // [1]
}
Maps¶
A Map stores key-value pairs, with each key appearing at most once.
fun main() {
val ages: Map<String, Int> = mapOf("Alice" to 30, "Bob" to 25)
println(ages) // {Alice=30, Bob=25}
println(ages["Alice"]) // 30
println(ages["Unknown"]) // null -- missing key returns null, not an exception
println(ages.getOrDefault("Unknown", 0)) // 0
println(ages.containsKey("Bob")) // true
val mutableAges: MutableMap<String, Int> = mutableMapOf()
mutableAges["Carol"] = 28
mutableAges["Dave"] = 35
mutableAges["Carol"] = 29 // overwrites the previous value
println(mutableAges) // {Carol=29, Dave=35}
}
The to in "Alice" to 30 creates a Pair — mapOf just takes a vararg
of pairs.
Iterating collections¶
fun main() {
val fruits = listOf("apple", "banana", "cherry")
for (fruit in fruits) {
println(fruit)
}
val ages = mapOf("Alice" to 30, "Bob" to 25)
for ((name, age) in ages) {
println("$name is $age")
}
// forEach as an alternative to a for loop
fruits.forEach { fruit -> println("Fruit: $fruit") }
}
Arrays (briefly)¶
Kotlin also has Array<T> (and primitive-specialized versions like
IntArray), fixed-size and mutable in place. Lists are preferred for most
everyday code; arrays show up mainly for performance-sensitive code or
interop with Java APIs.
fun main() {
val numbers = arrayOf(1, 2, 3)
numbers[0] = 99
println(numbers.joinToString()) // 99, 2, 3
val ints = intArrayOf(1, 2, 3) // specialized, avoids boxing
println(ints.sum()) // 6
}
How It Actually Works¶
Kotlin does not have its own collection runtime — List<String>,
MutableList<String>, Set, and Map all compile straight down to
java.util.List, java.util.Set, and java.util.Map. There is no
kotlin.collections.ArrayList class shipped separately at runtime; when you
write mutableListOf("apple", "banana"), the compiler emits a call to
kotlin.collections.CollectionsKt.mutableListOf(...), which internally just
constructs and returns a plain java.util.ArrayList. This is why Kotlin
collections interoperate seamlessly with Java libraries — at the bytecode
level, they are Java collections.
The read-only vs. mutable distinction (List vs MutableList) is therefore
a compile-time-only illusion layered on top of a single underlying Java
type. listOf(...) returns the exact same java.util.ArrayList instance
type that mutableListOf(...) does — it's just typed as List<String> by
the Kotlin compiler, which then refuses to let you call .add() on that
static type. If you cast that "read-only" List back to MutableList using
an unchecked cast or reflection and call .add() on it, it succeeds at
runtime, because the underlying object was mutable all along — the JVM never
enforced read-only-ness, Kotlin's type checker did. Genuinely immutable
collections (where mutation truly can't happen, even via casting) require
java.util.Collections.unmodifiableList or Kotlin's List.of-style
factories on newer stdlib versions.
Operations like .sorted(), .distinct(), and .reversed() each allocate a
new backing list rather than mutating or reordering the original — you
can confirm this by checking that numbers still prints in its original
order after calling numbers.sorted(). Internally, most of these are thin
wrappers that copy the elements into a fresh ArrayList, delegate to
java.util.Collections.sort (a well-tuned adaptive mergesort/Timsort
variant) or build a LinkedHashSet (for .distinct(), which relies on
hashCode()/equals() to detect duplicates while preserving insertion
order) and return that as the new list.
Cheat sheet¶
| Task | Syntax |
|---|---|
| Read-only list | listOf(1, 2, 3) |
| Mutable list | mutableListOf(1, 2, 3) |
| Read-only set | setOf(1, 2, 3) |
| Read-only map | mapOf("a" to 1, "b" to 2) |
| Mutable map | mutableMapOf<String, Int>() |
| Access by index | list[0] |
| Access map value | map["key"] (returns nullable) |
| Add to mutable list | list.add(value) |
| Iterate map | for ((k, v) in map) |
| Combine key+value | "key" to value |
🔀 See this in another language¶
Exercise¶
Write a program that builds a MutableMap<String, MutableList<String>>
representing students grouped by grade (e.g. "A" to mutableListOf("Alice",
"Anna")). Add at least three grades with a couple of students each. Then
print, for each grade in the map, the grade letter and the number of
students in it, followed by their names — sorted alphabetically within each
grade using sorted().