skip to content

In Kotlin, what is the difference between List and MutableList (and Set/MutableSet, Map/MutableMap), and how does the read-only/mutable split work?

level: juniorimportance: must knowfreq 85%

answer

  1. Two interface layers: read-only + Mutable* subtype
  2. MutableList IS-A List (upcast)
  3. Read-only != immutable (backing can change)
  4. listOf vs mutableListOf; buildList { }
  5. Compile-time distinction, same JDK collections at runtime

basics

~10 s

List 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 s

Kotlin 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 lines
kotlin
val 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

for a junior

Knows listOf vs mutableListOf and that List lacks add/remove while MutableList has them.

for a middle

Explains the interface hierarchy (MutableList : List) and that read-only is a view, not immutability.

for a senior

Discusses defensive copies, buildList, kotlinx persistent collections, and Java platform-type interop risks.

for a principal

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

context