Kotlin has no 'immutable' keyword or a frozen List type in the stdlib. Given only List/MutableList, how do you reason about and achieve actual immutability, and where do the standard interfaces stop?
answer
- No 'immutable List' interface in stdlib — only read-only
- read-only ≠ immutable (aliasing, cast, platform types)
- Immutability is transitive: elements must be immutable too
- kotlinx.collections.immutable: ImmutableList/PersistentList
- Stdlib trades enforced immutability for zero-cost Java interop
basics
~10 sThe List type only hides change methods; it is not a promise of immutability. To truly prevent changes you copy the data, keep the original hidden, or use a special immutable collection library.
solid answer
~40 sThe stdlib split has exactly two guarantees: `List` *doesn't declare* mutators, and `MutableList` does. There is **no third 'immutable List' interface** — read-only is not a frozen guarantee because the same object can be aliased as `MutableList`, downcast, or arrive as a Java platform type. To reason about real immutability you combine: (1) **encapsulation** — never leak the mutable backing reference (backing-property pattern, return `toList()` copies); (2) **deep immutability** — elements themselves must be immutable (`data class` with `val`s) or you only have a shallow guarantee; (3) **dedicated structures** — `kotlinx.collections.immutable` exposes `ImmutableList`/`PersistentList` whose mutators return new instances with structural sharing. The stdlib stops at compile-time intent; persistent collections and discipline provide the rest. This matters for thread-safety, defensive APIs, and predictable state in things like Compose or Flow.
code
kotlin · 8 linesimport kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.PersistentList
data class Point(val x: Int, val y: Int) // deeply immutable element
val base: PersistentList<Point> = persistentListOf(Point(0, 0))
val next = base.add(Point(1, 1)) // base unchanged, next is new
// next is also a read-only List<Point> and cannot be cast-and-mutatedgo deeper
Likely conflates read-only List with immutability; may not know any alternative.
Knows read-only is compile-time and that copies/encapsulation help but can't fully enforce immutability.
Distinguishes shallow vs deep immutability and reaches for kotlinx.collections.immutable when guarantees are needed.
Articulates the stdlib's interop-vs-enforcement trade-off and designs state/threading APIs around persistent immutable collections.
## What the stdlib actually promises There are only two interfaces in the relevant pair. `List` simply **omits** mutators; `MutableList` adds them. That's the entire contract. Key consequence: **read-only ≠ immutable**. The same runtime object can be reached through a `MutableList` reference (aliasing), downcast (`as MutableList`), or be a Java **platform type** (`List<String>!`) that is mutable. ## Three layers needed for real immutability ### 1. Reference encapsulation Keep the only mutable reference private; expose read-only views or copies. ```kotlin private val _items = mutableListOf<Item>() fun snapshot(): List<Item> = _items.toList() // new ArrayList, no aliasing ``` `toList()` defeats aliasing but the returned object is *still* a mutable `ArrayList` underneath — only your hiding of the reference makes it effectively immutable. ### 2. Element (deep) immutability A `List<MutableFoo>` can be 'read-only' yet each `Foo` is mutable, so the aggregate state changes. Real immutability is **transitive**: make elements immutable too (`data class Foo(val x: Int)`), otherwise you only have a **shallow** read-only container. ### 3. Persistent / immutable structures `kotlinx.collections.immutable` provides: - `ImmutableList`/`ImmutableSet`/`ImmutableMap` — marker interfaces promising no mutation through any reference. - `PersistentList` etc. — `add`/`remove` return a **new** collection sharing structure with the old (O(log n)-ish), leaving the original untouched. ```kotlin import kotlinx.collections.immutable.persistentListOf val a = persistentListOf(1, 2) val b = a.add(3) // a == [1,2], b == [1,2,3] ``` These give the guarantee the stdlib `List` does not, and they're heavily used where reference equality of state matters (Jetpack Compose stability, redux-style state). ## Why it matters at scale - **Thread-safety:** truly immutable data is freely shareable across threads/coroutines without locks; a leaked `MutableList` is a data race. - **API contracts:** returning a real `ImmutableList` lets callers cache, hash, and compare safely. - **Predictability:** frameworks that diff state (Compose `@Stable`/`@Immutable`, `StateFlow` updates) rely on values not changing under them. ## Where the stdlib intentionally stops Kotlin chose a **lightweight, JVM-interop-friendly** model: thin interfaces over `java.util` collections, no runtime freezing, no separate immutable class hierarchy in the core stdlib. The trade-off is zero-cost interop at the price of no enforced immutability — pushed to an opt-in library. Recognizing that boundary is the senior/principal-level insight.
- Why doesn't returning toList() give true immutability by itself?The returned ArrayList is still mutable; only hiding its reference makes it effectively immutable, and elements may still be mutable.
- What does 'shallow' read-only mean?The container exposes no mutators, but its elements are mutable objects, so the overall state can still change.
- Where does a real ImmutableList from kotlinx.collections.immutable help over List?It promises no mutation through any reference and supports safe sharing, caching, and stability annotations in Compose.
A read-only List is a glass case with the key still hanging on the wall; a persistent/immutable list welds the case shut and hands out copies.
saying these in an interview costs you the question
- Asserting List is Kotlin's immutable type
- Ignoring element mutability when claiming immutability
- Believing toList() returns a frozen/immutable structure
- Unaware of kotlinx.collections.immutable as the real solution
- Not seeing the interop-driven design rationale for the stdlib choice