How do Kotlin's read-only collection interfaces (List, Map, Set) map onto java.util types, and what does 'read-only' actually guarantee at runtime?
answer
- List & MutableList both → java.util.List
- Read-only = compile-time view, NOT immutable
- Same object can mutate via another reference
- Java always sees the full mutable java.util collection
- True immutability: kotlinx.immutable / toList() / unmodifiable
basics
~10 sKotlin's List/Set/Map and their MutableList/MutableSet/MutableMap versions all map to the same java.util.List/Set/Map at runtime. 'Read-only' is just a compile-time view: it hides the mutating methods but doesn't make the underlying object immutable.
solid answer
~30 skotlin.collections.List maps to java.util.List, Set to java.util.Set, Map to java.util.Map — these are mapped types, so a Kotlin List passed to Java is literally a java.util.List. Kotlin splits each into a read-only interface (List) and a Mutable* subtype (MutableList) that adds add/remove/put. Both project onto the SAME java.util interface; the distinction exists only in Kotlin's type system. So 'read-only' means 'this reference exposes no mutators', NOT 'immutable'. The same backing object can be mutated through another reference, or by Java code that always sees the full mutable java.util.List. Use kotlinx.collections.immutable or defensive copies for true immutability.
code
kotlin · 8 linesval backing = mutableListOf("a", "b")
val view: List<String> = backing // read-only reference, same instance
// view.add("c") // won't compile
backing.add("c") // mutates underlying object
println(view) // [a, b, c]
// Defensive copy for a stable snapshot
val snapshot = backing.toList() // independent read-only copygo deeper
Knows List/Map/Set map to java.util equivalents and that MutableList adds add/remove.
Explains that read-only is a compile-time view over the same object, not immutability, and that both faces erase to one java.util type.
Handles the Java boundary deliberately — defensive copies, unmodifiable wrappers, or immutable collections to preserve guarantees.
Sets API conventions for collection ownership/mutation across interop boundaries and chooses persistent collections where concurrency or sharing demands true immutability.
## The two-interface design Kotlin gives every collection two faces: - **Read-only**: `List<T>`, `Set<T>`, `Map<K,V>` — only query methods (`get`, `size`, `contains`, iteration). - **Mutable**: `MutableList<T>`, `MutableSet<T>`, `MutableMap<K,V>` — add the mutators (`add`, `remove`, `put`, `clear`). Each `Mutable*` is a subtype of the read-only one. ## How they map to java.util Both faces are **mapped types** that project onto the standard JVM collection interfaces: - `List` and `MutableList` → `java.util.List` - `Set` and `MutableSet` → `java.util.Set` - `Map` and `MutableMap` → `java.util.Map` - `Collection`/`MutableCollection` → `java.util.Collection` - `Iterable`/`MutableIterable` → `java.lang.Iterable`, `Iterator`/`MutableIterator` → `java.util.Iterator` At runtime there is only the java.util type. A Kotlin `List<String>` handed to Java arrives as a fully-featured `java.util.List<String>` — Java can call `add()` on it. ## What 'read-only' guarantees (and doesn't) Read-only is a **compile-time projection**, not immutability: - It guarantees: through *this* reference, you can't call mutators in Kotlin. - It does NOT guarantee: the underlying object never changes. The same instance may be held elsewhere as `MutableList`, or passed to Java that sees the mutable java.util.List and mutates it. ```kotlin val mutable = mutableListOf(1, 2, 3) val readOnly: List<Int> = mutable // same object, read-only view mutable.add(4) // mutates BOTH references println(readOnly) // [1, 2, 3, 4] ``` ## Calling Java that mutates If Java code calls `add()` on what Kotlin declared as read-only `List`, the JVM happily mutates it — Kotlin's guarantee is gone at the boundary. To be safe, pass a defensive copy or a truly immutable collection. ## True immutability Kotlin's read-only ≠ immutable. For deep guarantees use: - `kotlinx.collections.immutable` (`PersistentList`, `ImmutableList`) - defensive copies (`toList()` returns a new read-only copy) - `Collections.unmodifiableList(...)` for a runtime-enforced wrapper that throws on mutation ## Why the design exists It lets APIs express intent ("I won't modify this") and enables covariance: `List<out T>` is covariant, so `List<String>` is a `List<Any>`, which is unsafe for the mutable variant but fine for read-only.
- If a function takes List<T>, can a Java caller still add elements to it?Yes. The parameter is a java.util.List at runtime, so Java sees and can call add()/remove(). Kotlin's read-only constraint only applies to Kotlin code.
- How do you get a genuinely immutable collection in Kotlin?Use kotlinx.collections.immutable (PersistentList/ImmutableList), wrap with Collections.unmodifiableList (throws on mutation), or pass a defensive copy. Plain read-only List does not enforce immutability.
Read-only List is like a window showing a shared whiteboard with no marker in your hand — you can't write, but anyone else in the room (or a Java caller) still can, and you'll see the changes.
saying these in an interview costs you the question
- Saying read-only List is immutable
- Claiming List and MutableList are different java.util types
- Believing Java can't mutate a Kotlin read-only List
- Thinking toList() returns the same object
- Assuming the JVM enforces the read-only/mutable split