Does buildList return a truly immutable list? What can still mutate the contents, and what does the builder actually guarantee?
answer
- Read-only view, not deep immutable
- Backing object is a real ArrayList/MutableList
- Leaking the receiver = live mutation
- as MutableList downcast still works
- True immutability = kotlinx.collections.immutable
basics
~20 sNo. buildList returns a read-only view, not a deeply immutable list. You can't change it through the returned reference, but if you kept the mutable reference or the elements themselves are mutable objects, those can still change.
solid answer
~50 sbuildList returns `List<T>`, the read-only interface, backed by an ordinary `ArrayList`. The guarantee is narrow: the *returned reference* exposes no mutators, so callers can't add/remove through it. It is NOT true immutability. Two escape hatches exist. First, the builder lambda receives the same underlying mutable object; if you smuggle that receiver out (e.g. assign it to an outer var, or pass it to a function), code holding that reference can still mutate the list the read-only result points to. Second, `List` is read-only but the *elements* are not deep-frozen — if `T` is a mutable type, its fields can change. Also, a `List<T>` reference can be unsafely downcast (`as MutableList`) and mutated, since the runtime object really is a MutableList. For genuine immutability you'd use kotlinx.collections.immutable's persistent collections. buildList's value is ergonomics plus sealing the mutable reference at the lambda boundary so you don't have to defensively copy.
code
kotlin · 6 linesvar leak: MutableList<Int>? = null
val result: List<Int> = buildList {
add(1)
leak = this
}
leak!!.add(99) // result is now [1, 99] -- read-only is not immutablego deeper
Knows the result can't be changed via the returned List reference and writes add inside the block.
Distinguishes read-only from immutable and knows the backing object is a real MutableList.
Enumerates the escape hatches (receiver leak, unsafe downcast, mutable elements) and explains the builder only seals the mutable reference, no copy.
Decides API guarantees: when read-only-by-contract suffices vs. when to use kotlinx persistent collections, and how this affects defensive copying and thread-safety reasoning.
## Read-only ≠ immutable Kotlin distinguishes two ideas: - **Read-only interface** (`List`, `Set`, `Map`): exposes no mutator methods. `buildList` returns this. - **Immutable**: the object truly cannot change, by anyone, ever. `buildList` gives you the **read-only** one, not the immutable one. The backing object is a normal `java.util.ArrayList` (a `MutableList`) handed back upcast. ## What the builder DOES guarantee Within a normal usage, the only mutable reference to the new list is the lambda receiver, and that reference goes out of scope when the lambda returns. So the read-only result is *effectively* unmodifiable — no defensive `toList()` copy needed: ```kotlin fun ids(): List<Int> = buildList { add(1); add(2) } ``` ## Escape hatch 1: leaking the receiver If you capture the receiver, you keep a live mutable handle to the same object: ```kotlin var leak: MutableList<Int>? = null val result: List<Int> = buildList { add(1) leak = this // smuggle the mutable receiver out } leak!!.add(99) // result now contains [1, 99]! ``` This defeats the seal — don't do it. ## Escape hatch 2: unsafe downcast Because the runtime object is really a MutableList, `(result as MutableList<Int>).add(3)` compiles and runs. This is unsafe and discouraged but possible. ## Escape hatch 3: mutable elements Read-only means the *collection structure* is fixed, not the elements: ```kotlin val people: List<StringBuilder> = buildList { add(StringBuilder("a")) } people[0].append("b") // element mutated; the list is still read-only ``` ## When you need TRUE immutability Use `kotlinx.collections.immutable`: `persistentListOf`, `toImmutableList`, `PersistentList`. Those are structurally shared, genuinely immutable, and downcasting to mutable fails. `buildList` is not a substitute for them. ## Practical takeaway `buildList`/`buildSet`/`buildMap` are about **ergonomic construction + sealing the mutable reference**, giving you a read-only result without a copy. Treat the result as read-only-by-contract, not cryptographically frozen.
- If you need a genuinely immutable collection, what do you reach for?kotlinx.collections.immutable persistent collections (persistentListOf, PersistentList), not buildList.
- Why is downcasting a buildList result to MutableList possible at runtime?Because the backing object really is an ArrayList; the read-only typing is compile-time only, so a cast succeeds.
saying these in an interview costs you the question
- Claiming buildList produces an immutable / frozen list
- Saying you can never mutate the result (downcast and receiver leaks can)
- Confusing read-only structure with read-only elements
- Recommending buildList where true persistent immutability is required