skip to content

Explain why `val` does not guarantee immutability. How do you actually express an immutable value in Kotlin?

level: middleimportance: must knowfreq 78%

answer

  1. val = binding immutable; object can still mutate
  2. List is read-only, NOT immutable (backing may be ArrayList)
  3. data class + copy() for value semantics
  4. Don't leak the MutableList backing field
  5. kotlinx.collections.immutable for true immutability

basics

~20 s

val only stops you from pointing the name at a different object. The object it points to can still change if it is mutable. For real immutability you also need an immutable type, like List instead of MutableList.

solid answer

~40 s

`val` controls the *binding*, not the *object's internal state*. A `val` of type `MutableList<Int>` cannot be reassigned, but you can still call `add`/`remove` on it. True immutability requires the *type* to be immutable as well: use read-only interfaces like `List`, `Set`, `Map` (created via `listOf`, `mapOf`), or `data class` instances whose properties are all `val` and themselves immutable. Even read-only `List` is not a deep guarantee — the elements could be mutable, and the underlying object might be an `ArrayList` exposed elsewhere. For value semantics use `data class` + `copy()` to produce new instances instead of mutating. Kotlin also has experimental `@JvmInline value class` and immutable collections in `kotlinx.collections.immutable` (`persistentListOf`) for stronger guarantees.

code

kotlin · 5 lines
kotlin
class Cart {
    private val _items = mutableListOf<String>()  // mutable internally
    val items: List<String> get() = _items        // read-only outside
    fun add(item: String) { _items.add(item) }
}

go deeper

for a junior

Recognizes that val of a MutableList still allows add/remove.

for a middle

Distinguishes read-only List from immutable, and uses data class + copy() for value semantics.

for a senior

Avoids leaking backing collections and reaches for persistent/immutable collections when guarantees matter; reasons about aliasing.

for a principal

Designs APIs around immutability-by-default, weighing shallow vs deep immutability and concurrency implications.

## Binding immutability vs. object immutability There are two independent axes: 1. **Binding (reference) mutability** — can the *name* be pointed at a different object? Controlled by `val` (no) vs `var` (yes). 2. **Object (state) mutability** — can the *object's internal data* change? Controlled by the object's *type/design*, not by the keyword. `val` only addresses axis 1. ```kotlin val sb = StringBuilder("a") sb.append("b") // object mutated — still "ab" is fine // sb = StringBuilder() // ERROR — binding is frozen ``` ## Read-only vs. mutable collection interfaces Kotlin separates read-only and mutable collection *interfaces*: - `List`, `Set`, `Map` — **read-only** views (no `add`/`put`). - `MutableList`, `MutableSet`, `MutableMap` — expose mutators. ```kotlin val ro: List<Int> = listOf(1, 2) // ro.add(3) // no such method — read-only interface val mut: MutableList<Int> = mutableListOf(1, 2) mut.add(3) // allowed ``` Important nuance: `List` is **read-only, not immutable**. The actual runtime object is often an `ArrayList`. If some other reference holds it as `MutableList`, it can change underneath you: ```kotlin val backing = mutableListOf(1) val view: List<Int> = backing // read-only view backing.add(2) // view now sees [1, 2]! ``` ## Expressing genuine immutability - **`data class` with all-`val` properties** for value objects; evolve with `copy()` instead of mutation: ```kotlin data class Point(val x: Int, val y: Int) val p = Point(1, 2) val moved = p.copy(x = 5) // new instance; p unchanged ``` - **Don't leak the mutable backing collection** — expose `List`, build internally with `MutableList`. - **`kotlinx.collections.immutable`** — `persistentListOf`, `ImmutableList`/`PersistentList` give true structural immutability and sharing. - **`@JvmInline value class`** — wraps a single immutable value with no identity. ## Deep vs. shallow Even an all-`val` `data class` is only *shallowly* immutable if a property's type is itself mutable (e.g., a `val tags: MutableList<String>`). Deep immutability requires immutable types all the way down.

  • Is a read-only List truly immutable?
    No. It hides mutators, but the underlying object may be a MutableList held elsewhere and can change. For guaranteed immutability use persistent collections.
  • How do you safely expose internal mutable state?
    Keep a private MutableList backing field and expose it typed as List (often via a custom getter), so callers can't mutate it.

val is a locked car key holder — the key can't be swapped, but anyone can still drive the car around (mutate it).

saying these in an interview costs you the question

  • Equating val with deep immutability
  • Thinking listOf returns something unmodifiable that can never change via aliasing
  • Mutating an object returned from copy() and expecting the original to change
  • Exposing a public MutableList property instead of List
  • Confusing read-only (interface lacks mutators) with immutable (object truly can't change)

context