What does `.toList()` do versus simply upcasting a MutableList to List, and when must you prefer toList()?
answer
- Upcast = same object, no copy
- toList() = new independent O(n) copy
- Aliasing leaks through upcasts
- Copy at trust/lifetime boundaries (fields, returns, Java)
- toList is shallow; elements still shared
basics
~20 sUpcasting just changes the reference type but keeps the same object, so changes still leak through. toList() makes a brand-new copy, so later changes to the original don't affect it. Use toList() when you need a stable, independent snapshot.
solid answer
~40 sUpcasting (`val ro: List<T> = mutable`) is a no-op at runtime: same object, narrower reference type. The read-only reference can't mutate, but anyone holding the original MutableList — or a Java caller seeing java.util.List — still can, and the change is visible through the upcast (aliasing). `toList()` allocates a new, independent read-only collection (a defensive copy); subsequent mutations of the source don't affect it, and vice versa. Prefer `toList()` whenever you cross a trust boundary: storing a passed-in collection in a field, returning internal state, caching, or handing a snapshot to Java/another thread. It costs an O(n) copy but buys true isolation. For maps/sets use `toMap()`/`toSet()`; for guaranteed immutability across all callers reach for kotlinx.collections.immutable.
code
kotlin · 6 linesclass Cart(items: List<String>) {
// BAD: aliases caller's list — they can mutate our state later
// val items = items
// GOOD: defensive copy isolates internal state
val items: List<String> = items.toList()
}go deeper
Knows toList() makes a copy but may not contrast it precisely with upcasting.
Clearly distinguishes no-op upcast from O(n) defensive copy and copies at field/return boundaries.
Knows shallow-copy nuance, Java-interop motivation, and reaches for persistent collections when copies are costly.
Establishes encapsulation conventions (copy on store/return) and balances copy cost vs immutability across the codebase.
## Upcast: same object, narrower view ```kotlin val mutable = mutableListOf(1, 2, 3) val view: List<Int> = mutable // upcast — NO copy, runtime same object mutable.add(4) println(view) // [1, 2, 3, 4] — leak! ``` An upcast changes only what the **compiler** permits through `view`. The bytes are the same object. Any alias that can mutate (another `MutableList` reference, or a Java caller seeing `java.util.List`) changes what `view` reports. This is **aliasing**. ## toList(): a defensive copy ```kotlin val snapshot = mutable.toList() // NEW object, O(n) copy mutable.add(5) println(snapshot) // unchanged — isolated ``` `toList()` allocates a fresh collection containing the current elements. It is **decoupled** from the source: later mutations on either side don't cross over. (Note: it's a *shallow* copy — element objects themselves are shared, only the container is new.) ## When you MUST prefer toList() Use a defensive copy whenever a collection crosses a **trust or lifetime boundary**: - **Storing a parameter in a field:** `this.items = items.toList()` — otherwise the caller can mutate your internal state later. - **Returning internal state:** `return items.toList()` — otherwise callers can mutate your internals through the returned reference (especially a Java caller, who sees `java.util.List`). - **Java interop:** any collection received from or handed to Java should be copied if you need stability, since Java ignores the read-only marker. - **Concurrency/caching:** an independent snapshot avoids surprise mutations from other code paths. ## Cost and alternatives `toList()` is O(n) allocation + copy. If copies are too expensive and you need true immutability, use **`kotlinx.collections.immutable`** (`toPersistentList()` / `PersistentList`), whose structural sharing makes copies cheap and whose mutators throw for everyone. Sibling APIs: `toMutableList()`, `toSet()`, `toMutableSet()`, `toMap()`, `toMutableMap()`. ## Keywords/APIs upcast vs copy, aliasing, `toList`/`toMutableList`/`toSet`/`toMap`, shallow vs deep copy, defensive copy, `kotlinx.collections.immutable`.
- Is toList() a deep copy?No — it's shallow. The container is new, but element references are shared, so mutable elements can still be changed from outside.
- How do you avoid the O(n) copy cost while keeping immutability guarantees?Use kotlinx.collections.immutable persistent collections (toPersistentList), which share structure for cheap copies and are immutable to all callers.
Upcasting is putting a 'do not edit' cover sheet on a shared document; toList() is photocopying it so edits to the original never reach you.
saying these in an interview costs you the question
- Thinking upcasting to List creates a copy
- Storing a passed-in MutableList directly in a field
- Returning internal mutable lists without copying
- Believing toList() deep-copies the elements