How should the read-only vs mutable interface split inform the types you choose for function parameters, return values, and class properties?
answer
- Least privilege: read-only by default
- Param List accepts MutableList too
- Return List; copy via toList() to stop aliasing
- Backing-property: public val + private _mutable
- buildList gives scoped mutable, returns read-only
basics
~20 sUse 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.
solid answer
~40 sDefault to the **read-only** interface to follow the principle of least privilege. For **parameters**, take `List<T>`/`Set<T>`/`Map<K,V>` unless the function's contract is to mutate the caller's collection — read-only also accepts a wider range of arguments since `MutableList` is a `List`. For **return types**, return `List<T>` so callers can't accidentally mutate internals; if the value comes from a mutable backing field, return it as `List` and ideally a copy (`toList()`) to prevent aliasing leaks. For **properties**, expose `val items: List<T>` publicly while keeping a `private val _items = mutableListOf<T>()` backing — the classic backing-property pattern. Prefer `val` over `var` and read-only over mutable to make code easier to reason about. Reserve `MutableList` parameters for genuine builder/accumulator scenarios, and document that the function will modify the argument.
code
kotlin · 7 linesclass Inventory {
private val _items = mutableListOf<String>()
val items: List<String> get() = _items // expose read-only
fun add(item: String) { _items += item } // mutation behind a method
}
fun report(items: List<String>) = items.joinToString() // read-only paramgo deeper
Can pick List for reading and MutableList for changing in simple cases.
Applies least-privilege defaults and knows the backing-property pattern.
Reasons about aliasing leaks, defensive copies, and when buildList/immutable collections are warranted.
Sets team conventions for collection types in public APIs and balances encapsulation, performance, and interop.
## Principle: least privilege via types The split is an **API-design lever**. Each position (parameter, return, property) should request the *minimum* capability it needs. ## Parameters Prefer the read-only type: ```kotlin fun average(xs: List<Int>): Double = xs.sum().toDouble() / xs.size ``` Benefits: (1) the signature promises no mutation; (2) it accepts **both** `List` and `MutableList` arguments because `MutableList` is a subtype. Take `MutableList<T>` *only* when mutating the caller's collection is the explicit job (e.g. `fun fillDefaults(target: MutableList<T>)`), and say so in the doc. ## Return types Return `List<T>` to keep callers from mutating your internals: ```kotlin class Cart { private val _lines = mutableListOf<Line>() val lines: List<Line> get() = _lines // read-only view fun add(line: Line) { _lines += line } } ``` Note the view aliases `_lines`, so a downcast could still mutate it. If callers are untrusted or you need a true snapshot, return `_lines.toList()` (defensive copy) or an immutable type from `kotlinx.collections.immutable`. ## Properties: the backing-property pattern Expose a read-only `val` and hide a mutable backing field with a leading underscore (`_lines`). This is idiomatic Kotlin and keeps mutation centralized behind methods, preserving invariants. ## var vs val and read-only vs mutable Two orthogonal axes: - `val`/`var` controls whether the **reference** can be reassigned. - `List`/`MutableList` controls whether the **contents** can be changed through that reference. Idiomatic default: `val xs: List<T>` (neither reassignable nor mutable through this handle). Loosen one axis at a time, only when needed. ## When mutable is right - Local accumulators inside a function (`val acc = mutableListOf<T>()`) — never exposed. - Performance-sensitive builders; consider `buildList { ... }` which gives a scoped `MutableList` and returns a read-only `List`. - Explicit accumulator parameters in internal helpers. ## Summary table - Input you only read → `List`. - Input you must mutate → `MutableList` (documented). - Return value → `List` (copy if leaking internals matters). - Public property over mutable state → read-only `val` + private `_backing` mutable field.
- Why prefer List over MutableList for a parameter you only read?It documents 'no mutation', prevents accidental writes, and still accepts MutableList arguments via subtyping.
- The backing-property pattern returns a List view of a mutable field — what residual risk remains?Aliasing: a caller can downcast the view to MutableList. Return toList() or an immutable type if that matters.
saying these in an interview costs you the question
- Defaulting every parameter to MutableList 'just in case'
- Exposing a public mutable property and letting callers mutate internal state
- Confusing val/var with read-only/mutable
- Returning the live mutable backing field without considering aliasing
- Never reaching for toList() or immutable collections when encapsulation matters