skip to content

Explain Kotlin's read-only vs. mutable collection interfaces (List vs MutableList, etc.) and how the collection builders like buildList relate to them.

level: middleimportance: must knowfreq 70%

answer

  1. Read-only interface vs mutable subtype
  2. MutableList IS-A List
  3. Read-only != immutable (backing can change)
  4. buildList gives mutable receiver, returns read-only
  5. toList() for a defensive copy

basics

~20 s

List, Set and Map are read-only views — they have no add/remove. MutableList, MutableSet, MutableMap add modification methods. buildList gives you a mutable list to fill in a lambda and returns it as a read-only List.

solid answer

~40 s

Kotlin's collection hierarchy separates **read-only** interfaces (`List`, `Set`, `Map`, `Collection`, `Iterable`) from **mutable** ones (`MutableList`, `MutableSet`, `MutableMap`). The read-only interfaces expose only query operations (`size`, `get`, `contains`, `iterator`), while the mutable subtypes add `add`/`remove`/`put`/`clear`. This is **read-only, not immutable**: a `List` may still be backed by a mutable instance, and casting or aliasing can mutate it — there is no deep-freeze guarantee. Builders `buildList { }`, `buildSet { }`, `buildMap { }` give you a temporary mutable receiver inside the lambda and return a read-only result, which is the idiomatic way to construct a collection conditionally. `listOf()` and friends create read-only collections directly. All of this lives in the common stdlib, so it works identically in commonMain across targets.

code

kotlin · 6 lines
kotlin
fun headers(auth: String?, json: Boolean): Map<String, String> = buildMap {
    put("Accept", "*/*")
    if (json) put("Content-Type", "application/json")
    auth?.let { put("Authorization", "Bearer $it") }
}
// returns a read-only Map<String, String>

go deeper

for a junior

Knows List has no add and MutableList does, and can pick the right factory function.

for a middle

Correctly distinguishes read-only from immutable and uses buildList/buildMap idiomatically.

for a senior

Reasons about aliasing risks, defensive copies, and when to reach for persistent collections.

for a principal

Sets API-design conventions (return read-only types, copy at boundaries) and weighs immutability cost vs. safety across a codebase.

## Two parallel hierarchies Kotlin's collections come in two flavors: - **Read-only**: `Iterable` → `Collection` → `List` / `Set`, and `Map`. These declare only **query** methods: `size`, `isEmpty`, `contains`, `get`, `iterator`, plus the rich extension operators (`map`, `filter`, `first`). - **Mutable**: `MutableIterable` → `MutableCollection` → `MutableList` / `MutableSet`, and `MutableMap`. These **extend** the read-only ones and add mutators: `add`, `remove`, `set`, `put`, `clear`, plus a `MutableIterator` that supports `remove()`. Because `MutableList` **is a** `List`, you can pass a mutable list where a read-only one is expected — the receiver simply can't mutate through the read-only type. ## Read-only ≠ immutable This is the key subtlety. A `val xs: List<Int>` only means the **reference** is fixed and the **interface** exposes no mutators. The underlying object can still be a `MutableList` held elsewhere: ```kotlin val mutable = mutableListOf(1, 2, 3) val readOnly: List<Int> = mutable // same instance, read-only view mutable.add(4) println(readOnly) // [1, 2, 3, 4] <-- changed underneath you ``` For true immutability you need a defensive copy (`toList()`) or `kotlinx.collections.immutable` persistent collections. ## Builders The `build*` functions are the idiomatic constructors when building conditionally: ```kotlin val result = buildList { add("always") if (includeExtra) add("maybe") addAll(otherItems) } // type is List<String>, read-only ``` Inside the lambda the receiver is a `MutableList`; the returned value is a read-only `List`. There are `buildSet { }` and `buildMap { }` too. These are all **common** stdlib, available from `commonMain`. ## Defaults and copies - `listOf`, `setOf`, `mapOf` → read-only. - `mutableListOf`, `mutableSetOf`, `mutableMapOf` → mutable. - `toList()`, `toMutableList()`, `toSet()` → make copies, switching kinds. - `emptyList()` returns a shared, allocation-free empty instance.

  • How do you guarantee a returned list cannot be mutated by the caller's reference to your internal state?
    Return a defensive copy via toList(), or use a persistent collection from kotlinx.collections.immutable; the read-only type alone does not prevent aliasing.
  • Does buildList allocate twice?
    No — it constructs one ArrayList-backed list, runs the builder, then returns the same instance typed as read-only List (no copy).

A read-only List is like a window into a room: you can look in, but whoever holds the mutable key can still rearrange the furniture.

saying these in an interview costs you the question

  • Says List is immutable and can never change
  • Thinks MutableList and List are unrelated types
  • Believes buildList copies the collection on return
  • Cannot explain how read-only aliasing of a mutable list can still mutate

context