You return a `List<T>` from a function but it's backed by a private `MutableList<T>` field. What is the aliasing risk and how do `toList()` / `toMutableList()` defend against it?
answer
- up-cast = same object, no copy
- toList() / toMutableList() allocate fresh snapshot
- copy both on the way in and out at boundaries
- shallow copy: elements shared, container new
- cast-back attack: List as MutableList
basics
~20 sIf you return the same backing list, the caller's read-only reference still changes whenever your class mutates the field. Returning field.toList() makes a copy, so the caller gets a snapshot that can't change underneath them.
solid answer
~40 sUp-casting a `MutableList` to `List` does not copy — it returns the *same object* through a narrower interface. So `getItems(): List<T> = items` leaks an alias: every later `items.add(...)` is visible to the caller, and the caller could even cast back to `MutableList` and mutate your internal state. `toList()` creates a new read-only `List` containing the current elements (a defensive copy / snapshot); `toMutableList()` creates a new independent `MutableList`. Both break aliasing because mutating the original no longer affects the returned copy and vice versa. Note these are *shallow* copies — the element objects are shared. For value-like elements (immutable data classes) that's safe; for mutable elements you'd also need to copy them.
code
kotlin · 10 linesclass Cart {
private val items = mutableListOf("a", "b")
fun leaky(): List<String> = items // aliases internal state
fun safe(): List<String> = items.toList() // independent snapshot
}
val c = Cart()
val leak = c.leaky()
val snap = c.safe()
// items.add("c") inside -> leak shows [a,b,c], snap stays [a,b]go deeper
Recognizes that returning the field shares it and that toList() makes a copy.
Explains aliasing precisely, that up-cast doesn't copy, and uses toList()/toMutableList() correctly at boundaries.
Notes shallow vs deep, copy-on-input, cast-back attacks, and when copying is unnecessary for performance.
Designs encapsulation policy (defensive copying vs immutable element types vs persistent collections) and weighs allocation cost at scale.
## The leak Up-casting is free — it does not copy: ```kotlin class Cart { private val items = mutableListOf("a", "b") fun itemsLeaky(): List<String> = items // SAME object fun itemsSafe(): List<String> = items.toList() // COPY } ``` With `itemsLeaky()`, the returned `List` is the very same `ArrayList` the class keeps mutating: ```kotlin val cart = Cart() val view = cart.itemsLeaky() // later, internal code does items.add("c") println(view) // [a, b, c] <- caller's 'snapshot' changed ``` Worse, the caller can cast it back and corrupt your invariants: ```kotlin (cart.itemsLeaky() as MutableList<String>).clear() // mutates internal state ``` ## The defense: snapshot copies - **`toList()`** — returns a new read-only `List<T>` holding a snapshot of the current elements. - **`toMutableList()`** — returns a new independent `MutableList<T>` the caller may freely modify. Both allocate a fresh container, so the returned object and the source no longer alias each other: ```kotlin fun itemsSafe(): List<String> = items.toList() val snap = cart.itemsSafe() // items.add("c") later does NOT affect snap ``` ## Shallow, not deep `toList()`/`toMutableList()` copy the *container*, not the elements. The new list holds the **same element references**: ```kotlin data class Box(var n: Int) val src = mutableListOf(Box(1)) val copy = src.toList() src[0].n = 99 println(copy[0].n) // 99 -- same Box object shared ``` So defensive copying fully protects you only when elements are themselves immutable (e.g. `data class` with `val` fields). For mutable elements you must also copy each element. ## When to copy - Returning internal collection state from a class boundary → copy (`toList()`). - Storing a caller-supplied collection in a field → copy on the way *in* too, or the caller can mutate your state later. - Pure local transformation where no one aliases the source → copy unnecessary. ## Related APIs `toSet()`, `toMutableSet()`, `toMap()`, `toMutableMap()` give the same defensive-copy behavior for the other collection types. `List(n) { ... }` and `buildList { }` are other ways to produce fresh lists.
- Is `toList()` a deep copy?No, it's shallow: the new list shares the same element instances. Mutable elements can still be changed through either list.
- Do you also need to copy when *accepting* a collection into a field?Yes. If you store the caller's reference directly, they keep a handle to your internal state and can mutate it later. Copy on the way in (`init { this.items = arg.toMutableList() }`).
Returning the field directly is handing out a photocopy that magically updates when you edit the original; toList() hands out a real printout frozen in time.
saying these in an interview costs you the question
- Thinking up-casting MutableList to List copies the data
- Claiming toList() is a deep copy
- Only defending on the way out, not on the way in
- Returning the private MutableList field directly as List
- Believing the read-only return type prevents cast-back mutation