skip to content

Read-Only vs Mutable Interfaces

List, Set, and Map expose no mutators; the Mutable variants extend them and add add, remove, and set. Declaring parameters as the read-only type is a design habit interviewers look for, since it documents intent at the signature.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

In Kotlin, what is the difference between List and MutableList (and Set/MutableSet, Map/MutableMap)?

level: juniorimportance: must knowfreq 85%

answer

  1. Pairs: List/MutableList, Set/MutableSet, Map/MutableMap
  2. Read-only = no add/set/remove declared
  3. MutableX extends X and adds mutators
  4. [] is get; []= is set (only on mutable)
  5. Compile-time only; not true immutability

basics

~10 s

List only lets you read items. MutableList lets you also add, remove, or change them. The same idea applies to Set/MutableSet and Map/MutableMap.

solid answer

~40 s

Kotlin's standard library splits each collection into a read-only interface and a mutable one. List, Set, and Map expose only read operations: size, get/[], contains, iterator, and read-style functions. The MutableX interfaces extend their read-only parent and add mutators: MutableList.add/remove/set, MutableSet.add/remove, MutableMap.put/remove and []= via set. There is no separate immutable interface name — the read-only one IS List. So MutableList is a List, but a List variable forbids calling add at compile time. You pick the type at the variable/parameter/return position to express intent: declare List<T> when callers should not modify, MutableList<T> only when mutation is part of the contract. This is purely a compile-time, interface-level distinction, not a guarantee that the underlying object can never change.

code

kotlin · 6 lines
kotlin
fun sum(xs: List<Int>): Int = xs.sum()        // promises not to mutate
fun fill(xs: MutableList<Int>) { xs.add(0) }   // mutation is part of contract

val m = mutableListOf(1, 2)
fill(m)                                        // MutableList is a List, also accepted by sum
println(sum(m))                                // 3

go deeper

for a junior

Knows List is read-only and MutableList lets you add/remove; can pick the right one for a variable.

for a middle

Articulates that MutableList extends List and that the choice expresses intent in APIs.

for a senior

Distinguishes read-only from immutable and explains the compile-time-only nature plus aliasing risks.

for a principal

Frames the split as an API-design tool for least-privilege contracts and discusses how it interacts with platform/Java collections.

## The interface split Kotlin's collections live in `kotlin.collections` and come in **pairs**: - `List<out E>` / `MutableList<E>` - `Set<out E>` / `MutableSet<E>` - `Map<K, out V>` / `MutableMap<K, V>` - plus `Collection<out E>` / `MutableCollection<E>` and `Iterable`/`MutableIterable` as parents. The **read-only** interface (e.g. `List`) declares only operations that *observe* the collection: `size`, `isEmpty()`, `contains(e)`, `get(index)` (the `[]` operator), `indexOf`, `iterator()`, plus the many extension functions (`map`, `filter`, `first`…) that return new collections. The **mutable** interface (e.g. `MutableList`) **extends** the read-only one and adds *mutators*: - `MutableList<E>`: `add`, `remove`, `set` (the `[]=` operator), `removeAt`, `clear`, plus a `MutableListIterator`. - `MutableSet<E>`: `add`, `remove`, `addAll`, `clear`. - `MutableMap<K,V>`: `put` (the `[]=` operator), `remove`, `clear`, and `entries` of type `MutableMap.MutableEntry`. ```kotlin val read: List<Int> = listOf(1, 2, 3) // read.add(4) // compile error: List has no add val mutable: MutableList<Int> = mutableListOf(1, 2, 3) mutable.add(4) // OK mutable[0] = 99 // OK, uses set() ``` ## Why it matters The split lets you **express intent in the type**. A function that takes `List<T>` promises not to mutate the argument; a function returning `List<T>` tells callers "don't try to change this." You only widen to `MutableList<T>` when mutation is genuinely part of the contract. ## Read-only is not immutable This is a **compile-time** distinction. `List` simply doesn't *declare* mutators — it does **not** guarantee the object never changes. The same object can be referenced through both a `MutableList` and a `List` variable; mutating via the mutable reference is visible through the read-only one. True immutability is a separate concern (covered by read-only *views* vs truly immutable structures). ## Subtyping Because `MutableList` extends `List`, every `MutableList` *is* a `List`, so you can pass a `MutableList` where a `List` is expected — the callee just sees fewer operations.

  • Can you pass a MutableList to a function expecting a List?
    Yes. MutableList extends List, so it is a subtype; the function just can't call mutators.
  • Does declaring a parameter as List make the collection immutable?
    No. It only hides mutators at compile time; the underlying object may still be mutated elsewhere.

A read-only List is a museum exhibit you can look at; a MutableList is the same display but with a sign saying 'staff may rearrange'.

saying these in an interview costs you the question

  • Claiming List is 'immutable' and can never change
  • Thinking MutableList and List are unrelated types rather than subtype/supertype
  • Saying you cannot pass a MutableList where a List is required
  • Confusing read-only interface with Java's Collections.unmodifiableList semantics by name only

context

open as a page

Why is List<out E> declared covariant but MutableList<E> invariant in Kotlin?

level: middleimportance: should knowfreq 55%

basics

~10 s

A read-only List<Cat> can safely be treated as a List<Animal> because you only read from it. A MutableList<Cat> cannot, because you could try to add a Dog, so it must stay exactly its type.

open as a page

How should the read-only vs mutable interface split inform the types you choose for function parameters, return values, and class properties?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Use the read-only type (List, Set, Map) almost everywhere — for inputs you only read and for things you return. Only use the Mutable type when the function or caller really needs to change the collection.

open as a page

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?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The 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.

open as a page

Kotlin has no 'immutable' keyword or a frozen List type in the stdlib. Given only List/MutableList, how do you reason about and achieve actual immutability, and where do the standard interfaces stop?

level: principalimportance: nice to knowfreq 25%

basics

~10 s

The List type only hides change methods; it is not a promise of immutability. To truly prevent changes you copy the data, keep the original hidden, or use a special immutable collection library.

open as a page