Mutable versus read-only collections
This is one of the most important distinctions in Kotlin, and one beginners routinely miss because
the two versions look almost identical. A Kotlin collection is either read-only or mutable,
and choosing correctly is a large part of writing safe, clear code — the collection version of the
val-by-default habit.
Two families
Every collection type comes in two flavours:
val readOnly = listOf(1, 2, 3) // List — cannot be changed
val mutable = mutableListOf(1, 2, 3) // MutableList — can be changed
readOnly.add(4) // error: unresolved reference: add — no such method
mutable.add(4) // fine — [1, 2, 3, 4]
listOf gives a List, which has no methods to change it — no add, no remove, no set.
mutableListOf gives a MutableList, which adds those methods. Same for the others:
val s = setOf(1, 2) val ms = mutableSetOf(1, 2) // add/remove
val m = mapOf("a" to 1) val mm = mutableMapOf("a" to 1) // put/remove
The read-only version is not a different collection under the hood — it is the same data exposed through an interface that does not let you modify it. That distinction matters, and there is a subtlety about it below.
Read-only by default — the habit
Reach for the read-only version (listOf, setOf, mapOf) every time. Use the mutable version
only when you genuinely need to change the collection after creating it. This is exactly the
val-versus-var philosophy applied to collections, and the reasons are the same:
- A read-only collection cannot surprise you. When a function takes a
List, you know it cannot add to or remove from it — so passing your list to it is safe. With aMutableList, the function could modify your data, and you would have to check. - It communicates intent.
Listsays "this is a fixed set of items";MutableListsays "this is expected to change". - It prevents accidental modification — a whole class of bug where a collection changes when you did not expect it to, because some far-away code held a mutable reference.
Most collections, once built, are only read — filtered, mapped, iterated. Those should be List,
not MutableList.
val versus read-only — do not confuse them
Here is the trap that catches everyone, and it is the same one from the val lesson. val and
read-only are two different axes, and they combine into four possibilities:
val a = listOf(1, 2, 3) // val + read-only: cannot reassign, cannot modify
var b = listOf(1, 2, 3) // var + read-only: CAN reassign to a new list, cannot modify contents
val c = mutableListOf(1, 2, 3) // val + mutable: cannot reassign, CAN modify contents
var d = mutableListOf(1, 2, 3) // var + mutable: both
valcontrols whether the variable can point at a different collection.listOfversusmutableListOfcontrols whether the collection can be changed.
The one people get wrong: val c = mutableListOf(...). The val does not make the list
immutable — you cannot reassign c, but you can still c.add(4) all day, because the list is
mutable. If you want a genuinely unchangeable list, you need val and listOf. Getting this
straight is the whole lesson:
val fixed = listOf(1, 2, 3)
fixed.add(4) // error — the list itself has no add
// this is truly immutable: neither reassignable nor modifiable
Building a read-only collection from a mutable one
A common and clean pattern: build with a mutable collection, then expose it as read-only.
fun activeCities(): List<String> {
val result = mutableListOf<String>() // mutable while building
for (city in allCities) {
if (city.isActive) result.add(city.name)
}
return result // returned as List — callers cannot modify it
}
The function uses a MutableList internally to accumulate, but its return type is List, so the
caller receives something they cannot change. This is the best of both: mutation where it is
convenient (inside, building), immutability where it matters (the interface). Though, as the loops
and lambdas lessons hinted, you would more often write this as allCities.filter { it.isActive } .map { it.name } — which returns a read-only List directly, no mutable list needed.
The honest caveat: read-only is not deeply immutable
One subtlety to be aware of, because it can bite. A read-only List is a view that does not let
you modify it — but it does not guarantee the underlying data never changes. If you create a
MutableList and assign it to a List reference, the List reference is read-only, yet the
original mutable reference can still change the data:
val mutable = mutableListOf(1, 2, 3)
val readOnlyView: List<Int> = mutable // a read-only view of the same data
mutable.add(4) // changed through the mutable reference
println(readOnlyView) // [1, 2, 3, 4] — the "read-only" view changed!
So List means "you cannot modify it through this reference", not "this data is frozen forever".
In practice this rarely causes trouble if you follow the habit — build with a mutable collection,
return it as read-only, and do not keep the mutable reference around. But it is worth knowing that
Kotlin's read-only collections are about access, not a deep immutability guarantee. (For a genuine
defensive copy, mutable.toList() gives you an independent read-only list that will not change.)
Check your work
The two families of every collection. Read-only (listOf) and mutable (mutableListOf).
What the read-only version lacks. Methods to change it — no add, remove, or set.
The default to reach for, and why. Read-only — it cannot surprise you, communicates intent, and prevents accidental modification.
The two independent axes. val/var (can the variable be reassigned) and read-only/mutable (can
the collection be changed).
What val c = mutableListOf(...) allows. You cannot reassign c, but you can still modify the
list — val does not make a collection immutable.
How to get a genuinely unchangeable list. val and listOf.
The build-then-expose pattern. Use a MutableList internally, return type List.
Why read-only is not deep immutability. It restricts modification through that reference; the
underlying data can still change through a mutable reference — use toList() for a defensive copy.
Practice
- Create a
listOfand try toaddto it. Read the error. Then do the same withmutableListOfand confirm it works. - Write all four combinations of
val/varand read-only/mutable, and for each, try both reassigning the variable and modifying the collection. Predict which two operations each allows. - Take
val c = mutableListOf(1, 2, 3), add to it (works), then try to reassignc(fails). Explain both. - Write
activeCities()that builds with aMutableListand returns aList. Confirm the caller cannot modify the result. - Rewrite the same function as a
filter/mapchain and confirm it returns a read-only list. - Reproduce the read-only-view caveat: assign a
MutableListto aListreference, modify through the mutable one, and watch the read-only view change. Then fix it withtoList(). - Go through code you have written and, for each mutable collection, decide whether it needed to be mutable.
Official documentation
- Kotlin — Collections: mutable and read-only — The two interface families.
- Kotlin — Coding conventions: immutability — The guidance to prefer read-only.
- Kotlin standard library — toList — Making an independent read-only copy.
Next: map, filter, reduce and the operations that replace most of your loops.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship