RizTech Academy logo
RizTech Academy
CollectionsLesson 2 of 520 min

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 a MutableList, the function could modify your data, and you would have to check.
  • It communicates intent. List says "this is a fixed set of items"; MutableList says "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
  • val controls whether the variable can point at a different collection.
  • listOf versus mutableListOf controls 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

  1. Create a listOf and try to add to it. Read the error. Then do the same with mutableListOf and confirm it works.
  2. Write all four combinations of val/var and read-only/mutable, and for each, try both reassigning the variable and modifying the collection. Predict which two operations each allows.
  3. Take val c = mutableListOf(1, 2, 3), add to it (works), then try to reassign c (fails). Explain both.
  4. Write activeCities() that builds with a MutableList and returns a List. Confirm the caller cannot modify the result.
  5. Rewrite the same function as a filter/map chain and confirm it returns a read-only list.
  6. Reproduce the read-only-view caveat: assign a MutableList to a List reference, modify through the mutable one, and watch the read-only view change. Then fix it with toList().
  7. Go through code you have written and, for each mutable collection, decide whether it needed to be mutable.

Official documentation

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