skip to content

In Kotlin, what does it mean that a `List` is read-only, and why is that not the same as being immutable?

level: juniorimportance: must knowfreq 70%

answer

  1. read-only = no mutators on the interface
  2. immutable = object can never change for anyone
  3. List can be backed by ArrayList
  4. aliasing: MutableList + List point at same object
  5. type-level guarantee, not object-level

basics

~10 s

A read-only List only lacks add/remove methods on its interface. The actual object behind it can still change through another reference. Read-only means 'you can't change it', not 'nobody can'.

solid answer

~30 s

Kotlin's `List<T>` interface exposes no mutators (`add`, `remove`, `set`), while `MutableList<T>` adds them. But `List` is just a *view*: at runtime the object is usually a `java.util.ArrayList`. If another reference holds the `MutableList` (or someone casts the `List` back to `MutableList`), it can mutate, and the change is visible through the read-only reference too. So read-only restricts the *interface you have*, not the *object's mutability*. True immutability would guarantee no one can ever mutate it, which Kotlin's standard `List` does not provide. `listOf(...)` returns an effectively-unmodifiable instance, but that's an implementation detail, not the type-level guarantee that `List` gives.

code

kotlin · 4 lines
kotlin
val mutable: MutableList<Int> = mutableListOf(1, 2, 3)
val readOnly: List<Int> = mutable
mutable.add(4)
println(readOnly) // [1, 2, 3, 4] -- read-only view saw the change

go deeper

for a junior

Knows List lacks add/remove and MutableList has them; can state read-only != immutable.

for a middle

Explains aliasing — a List reference over a MutableList object sees changes; backing ArrayList.

for a senior

Distinguishes type-level guarantee from runtime class; mentions defensive copying and kotlinx.collections.immutable.

for a principal

Frames API design implications: why Kotlin chose read-only views over true immutability, interop trade-offs with Java mutable collections.

## Read-only vs immutable Kotlin splits collections into two interface families: - **Read-only**: `List<T>`, `Set<T>`, `Map<K,V>`, `Collection<T>`, `Iterable<T>` — they expose only *accessors* (`get`, `size`, `contains`, `iterator`). No `add`, `remove`, `set`, `clear`. - **Mutable**: `MutableList<T>`, `MutableSet<T>`, `MutableMap<K,V>`, `MutableCollection<T>` — they extend the read-only ones and add the mutators. A variable typed as `List<T>` simply *cannot call* mutating methods — the compiler hides them. That is **read-only**. **Read-only is a property of the reference, not the object.** The same underlying object can be reachable through both a `List` reference and a `MutableList` reference: ```kotlin val mutable: MutableList<Int> = mutableListOf(1, 2, 3) val readOnly: List<Int> = mutable // same object, narrower view mutable.add(4) println(readOnly) // [1, 2, 3, 4] <-- changed under us! ``` Here `readOnly` and `mutable` point at the same `ArrayList`. The read-only reference gives you no `add`, but it does not freeze the object. This is called **aliasing**. ## Why not just immutable? Kotlin's `List` is **not** an immutable type. There is no compile-time or runtime guarantee that the contents never change. Contrast with a truly immutable collection (e.g. from `kotlinx.collections.immutable`'s `PersistentList`, or a defensive copy you never share), which guarantees the instance can never change once created. ## How `listOf` behaves `listOf(1, 2, 3)` happens to return an instance whose backing array you cannot mutate even via reflection-free casts in practice, but **the type is still `List`**, so you still only get a read-only guarantee at the type level. Do not rely on the concrete class. ## Key terms - **Read-only**: interface with no mutators. - **Immutable**: the object can never change, for anyone. - **Aliasing**: two references to one object; mutating via one is seen via the other. - **View**: a reference that exposes a subset of an object's capabilities.

  • Can you mutate a `List<T>` by casting it back to `MutableList<T>`?
    If the underlying object really is mutable, yes — `(list as MutableList).add(x)` works. It is unsafe: the object may be a genuinely unmodifiable instance, which throws `UnsupportedOperationException`.
  • Does `listOf()` return an immutable list?
    It returns a read-only `List`. The instance is effectively unmodifiable, but the *type* only promises read-only, so you should not depend on the concrete class.

Read-only is a window into a room: you can look but not touch — yet someone with the key can still rearrange the furniture.

saying these in an interview costs you the question

  • Saying `List` is immutable / can never change
  • Believing read-only freezes the object itself
  • Confusing 'no mutator methods' with 'no mutation possible'
  • Claiming Kotlin has built-in persistent/immutable collections in the stdlib
  • Thinking `val list` makes the contents immutable (it only fixes the reference)

context