A function returns List<String>, yet a caller manages to mutate it at runtime. How is that possible, and how do the read-only interfaces relate to the actual JVM collection classes?
answer
- Read-only = interface hides mutators, runtime class is often ArrayList
- ro is MutableList<*> is often true
- Aliasing + cast + platform types = leaks
- toList() = defensive copy, not immutable
- kotlinx.collections.immutable for real immutability
basics
~20 sThe read-only type only hides change methods at compile time. The real object underneath is often a normal mutable ArrayList, so anyone holding a mutable reference to it, or a cast, can still change it.
solid answer
~40 sThe read-only/mutable split is a Kotlin **compile-time** abstraction. At runtime there are no separate immutable classes for the standard factories: `listOf`/`mutableListOf` typically both produce a `java.util.ArrayList` (or a singleton for size 0/1). `List<String>` just *declares* fewer methods. Mutation happens when (a) some other reference holds the same object as a `MutableList` and mutates it — visible through the read-only alias; or (b) code casts back: `(myList as MutableList<String>).add("x")` — which compiles and, on an ArrayList, succeeds. There's also the **platform-type** angle: a value coming from Java is `(Mutable)List<String>!` and Kotlin may treat it as mutable. To get a real defensive copy, return `list.toList()` (a new list) — still not truly immutable, but a separate object. For genuine immutability use `kotlinx.collections.immutable` (`PersistentList`).
code
kotlin · 8 linesval backing = mutableListOf("a", "b")
val view: List<String> = backing // same object
@Suppress("UNCHECKED_CAST")
(view as MutableList<String>).add("c") // compiles & works on ArrayList
println(backing) // [a, b, c]
val safe: List<String> = backing.toList() // defensive copygo deeper
May only know List 'can't be changed' and be surprised it can be cast.
Knows read-only is compile-time and that the backing object is often a mutable ArrayList.
Explains aliasing, downcasting, platform types, and chooses toList() vs immutable collections appropriately.
Designs APIs for encapsulation, weighs defensive copies vs persistent collections, and reasons about cross-language (Java interop) mutation risks.
## Read-only is an interface contract, not a class Kotlin's `List`/`MutableList` are **interfaces in `kotlin.collections`**. On the JVM they are *mapped* to and backed by the standard Java collections. Crucially, the same concrete class usually implements **both** interfaces: ```kotlin val ro: List<String> = mutableListOf("a") // runtime object is java.util.ArrayList println(ro is MutableList<*>) // true ``` So a `List<String>` value is frequently *actually* a `MutableList` at runtime; the read-only type just doesn't expose the mutators. ## Three ways a read-only list still mutates 1. **Aliasing.** If you build a `MutableList`, mutate via that reference, and also expose it as `List`, the read-only view reflects every change — it's the same object. ```kotlin val backing = mutableListOf(1, 2) val exposed: List<Int> = backing backing.add(3) println(exposed) // [1, 2, 3] ``` 2. **Downcasting.** Because the runtime class implements `MutableList`, `(exposed as MutableList<Int>).add(99)` compiles (with an unchecked-cast warning) and works. 3. **Platform types from Java.** A Java method returning `List<String>` gives Kotlin a **platform type** written `(Mutable)List<String>!`. Kotlin lets you assign it to either `List` or `MutableList`, and the underlying Java list may be mutable. ## Special non-mutable runtime objects Some factories *do* return objects that throw on mutation: - `emptyList()`/`listOf()` → a shared singleton `EmptyList` that has no elements and rejects adds. - `listOf(x)` (single arg) → `Collections.singletonList`, which throws `UnsupportedOperationException` on `add`. - `List(n) { ... }` and `listOf(a, b, c)` → an `ArrayList`. So casting and mutating is **not** universally safe — it depends on the concrete backing object. ## Defensive copying vs true immutability - `list.toList()` / `list.toMutableList()` create a **new** `ArrayList` — breaks aliasing but is itself mutable internally. - For real immutability, use **`kotlinx.collections.immutable`**: `persistentListOf()`, `toImmutableList()`, `PersistentList`/`ImmutableList`. These provide persistent (structural-sharing) data structures with `add` returning a new instance. ```kotlin import kotlinx.collections.immutable.persistentListOf val p = persistentListOf(1, 2) val p2 = p.add(3) // p unchanged, p2 is new ``` ## Takeaway The `List`/`MutableList` split buys you **encapsulation and intent**, not safety guarantees. Treat a returned `List` as read-only by convention; if you must guarantee the caller can't mutate your internal state, return a copy or an immutable collection.
- What does listOf("x") with one argument return at runtime, and what happens if you cast it and call add?A Collections.singletonList; add throws UnsupportedOperationException — unlike a multi-element ArrayList.
- How do you guarantee a returned collection truly cannot be mutated by callers?Return a persistent/immutable type (kotlinx.collections.immutable) or at minimum a defensive copy plus discipline.
saying these in an interview costs you the question
- Claiming List<String> guarantees the object is immutable at runtime
- Not knowing listOf and mutableListOf often produce the same ArrayList class
- Unaware of platform types from Java affecting mutability
- Recommending the cast-and-add as a normal technique rather than a leak
- Thinking toList() returns an immutable structure