In Kotlin, what is the difference between List and MutableList (and Set/MutableSet, Map/MutableMap), and how does the read-only/mutable split work?
answer
- Two interface layers: read-only + Mutable* subtype
- MutableList IS-A List (upcast)
- Read-only != immutable (backing can change)
- listOf vs mutableListOf; buildList { }
- Compile-time distinction, same JDK collections at runtime
basics
~10 sList lets you only read elements; MutableList also lets you add, remove, or change them. The same idea applies to Set/MutableSet and Map/MutableMap. You pick the type based on whether the data should change.
solid answer
~30 sKotlin splits each collection into a read-only interface (List, Set, Map) and a Mutable* sub-interface (MutableList, MutableSet, MutableMap). The read-only interfaces expose only query operations (size, get, contains, iteration); the Mutable* ones add mutators (add, remove, put, clear). You create them with listOf/setOf/mapOf (read-only) or mutableListOf/mutableSetOf/mutableMapOf (mutable). The read-only views are not immutable: they are interfaces over a possibly-mutable backing object, so a List reference can be upcast from a MutableList that someone else still mutates. For true immutability you copy or use kotlinx.collections.immutable. This split is compile-time only; at runtime both map to the same JDK collections (e.g. ArrayList).
code
kotlin · 9 linesval nums = mutableListOf(1, 2, 3)
val view: List<Int> = nums // read-only reference, same object
// view.add(4) // won't compile: List has no add
nums.add(4)
println(view) // [1, 2, 3, 4]
val frozen = nums.toList() // defensive copy; independent snapshot
nums.add(5)
println(frozen) // [1, 2, 3, 4]go deeper
Knows listOf vs mutableListOf and that List lacks add/remove while MutableList has them.
Explains the interface hierarchy (MutableList : List) and that read-only is a view, not immutability.
Discusses defensive copies, buildList, kotlinx persistent collections, and Java platform-type interop risks.
Reasons about API design: exposing read-only types at boundaries, immutability guarantees, and trade-offs of copying vs persistent data structures for shared state.
## The core idea Kotlin does NOT have separate runtime collection classes for "immutable" and "mutable". Instead it provides two layers of **interfaces**: - **Read-only interfaces**: `Collection<E>`, `List<E>`, `Set<E>`, `Map<K, V>`. These declare only *query* members: `size`, `isEmpty()`, `contains()`, `get()`/`[]`, `iterator()`, plus index/key access. - **Mutable sub-interfaces**: `MutableCollection<E>`, `MutableList<E>`, `MutableSet<E>`, `MutableMap<K, V>`. Each *extends* its read-only parent and adds *mutators*: `add()`, `remove()`, `set()`, `put()`, `clear()`, `removeAt()`, etc. Because `MutableList<E> : List<E>`, every `MutableList` *is a* `List`, so you can pass a mutable list where a read-only one is expected (this is an upcast). ## Read-only is NOT immutable A `List` reference only restricts what *you* can do through that reference. The underlying object may still be a `MutableList` that another holder mutates: ```kotlin val mutable = mutableListOf(1, 2, 3) val readOnly: List<Int> = mutable // upcast, same object mutable.add(4) println(readOnly) // [1, 2, 3, 4] — changed underneath! ``` This is sometimes called the *read-only view* problem. For genuine immutability, copy with `toList()`/`toMutableList()`, or use the `kotlinx.collections.immutable` library (`PersistentList`, `toPersistentList()`). ## Builder functions - Read-only: `listOf()`, `setOf()`, `mapOf()` (and `emptyList()`, `listOfNotNull()`). - Mutable: `mutableListOf()`, `mutableSetOf()`, `mutableMapOf()`, plus the builder DSLs `buildList { }`, `buildSet { }`, `buildMap { }` which give you a mutable receiver and return a read-only result. ## Runtime mapping These are mostly *mapped types* over JDK collections. `listOf()` typically returns a `java.util.Arrays$ArrayList`-style or a Kotlin singleton; `mutableListOf()` returns a `java.util.ArrayList`. The mutable/read-only distinction is enforced by the Kotlin compiler at compile time, not by the JVM at runtime — calling a mutator on a read-only type simply isn't visible in the API. ## Java interop caveat A Java method returning `java.util.List` is seen in Kotlin as a *platform type* `(Mutable)List!`, so the read-only guarantee can be bypassed. Also `listOf()` results may throw `UnsupportedOperationException` if a consumer casts and tries to mutate.
- If List is read-only, why can a List still change while you hold it?Because read-only means the *interface* lacks mutators, not that the object is immutable. The same object may be held elsewhere as a MutableList and mutated. Use toList() to take a snapshot.
- How do you guarantee a returned collection cannot be mutated by callers?Return a defensive copy (toList()) so callers get an independent snapshot, or use kotlinx.collections.immutable persistent collections which truly cannot be modified.
A read-only List is like a window into a room: you can see what's there, but someone with the key (the MutableList reference) can still rearrange the furniture.
saying these in an interview costs you the question
- Claiming List in Kotlin is immutable / cannot change
- Thinking there are distinct runtime classes for read-only vs mutable
- Saying you must cast a List to add elements (you'd need the actual MutableList)
- Confusing val (reference can't be reassigned) with collection immutability
- Believing mapOf returns something you can put() into