Skip to content

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]
}
[apple, banana, cherry]
apple
3
[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]
}
5
3
true
2
[1, 3, 3, 5, 8, 9]
[3, 9, 1, 8, 3, 5]
9
1
29
[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]
}
[1, 2, 3]
[a, b, c]
[1, 2, 3, 4]
[2, 3]
[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}
}
{Alice=30, Bob=25}
30
null
0
true
{Carol=29, Dave=35}

The to in "Alice" to 30 creates a PairmapOf 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") }
}
apple
banana
cherry
Alice is 30
Bob is 25
Fruit: apple
Fruit: banana
Fruit: cherry

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
}
99, 2, 3
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().