You declare `fun process(items: List<String>)` in Kotlin and call it from Java. Can the Java caller add elements to that list? Explain.
answer
- Read-only List compiles to java.util.List
- Java sees add/remove regardless of Kotlin intent
- Mutation visible to Kotlin via aliasing
- Immutable backing -> UnsupportedOperationException
- Defensive copy with toList() at the boundary
basics
~10 sYes. Kotlin's read-only List is just a java.util.List to Java, so Java sees the normal add and remove methods and can change the list, even though Kotlin marked the parameter as read-only.
solid answer
~40 sYes. Kotlin's read-only/mutable split is erased at the JVM boundary: kotlin.collections.List is compiled to (and seen by Java as) java.util.List, which is fully mutable. A Java caller therefore sees add/remove/set and can mutate the very object you passed — the read-only contract is a Kotlin-compiler fiction Java cannot see. If the underlying object is actually mutable (e.g. you passed a mutableListOf or an ArrayList), the mutation succeeds and your Kotlin code observes the change through aliasing. If you passed an immutable backing (listOf with no mutable backing, or a defensively copied unmodifiable list), Java's add throws UnsupportedOperationException at runtime. Lesson: never assume a List parameter is safe from external mutation when Java interop is involved — defensively copy with toList() if you need a stable snapshot.
code
kotlin · 8 lines// Kotlin
fun process(items: List<String>) {
val snapshot = items.toList() // defend against external mutation
// ... use snapshot, immune to caller's later add()
}
// Java caller can still do: data.add("x") on the original object,
// because Kotlin's List is java.util.List on the JVM.go deeper
May guess wrongly; can state that List has no add in Kotlin but unsure about the Java view.
Knows Java sees java.util.List and can mutate, naming UnsupportedOperationException for immutable backings.
Explains the JVM erasure of the split, aliasing visibility, and defensively copies at boundaries.
Designs interop contracts to prevent mutation leaks, weighing defensive copies vs persistent collections vs documented ownership.
## The boundary erases the distinction Kotlin's `List` and `MutableList` are **distinct only in Kotlin source**. On the JVM there is one interface: `java.util.List`. When Kotlin compiles `fun process(items: List<String>)`, the signature Java sees is `void process(List<String> items)` using `java.util.List` — the **full read/write** interface. So from Java: ```java List<String> data = new ArrayList<>(List.of("a", "b")); KotlinKt.process(data); // inside process, items is read-only to Kotlin... // but Java still has the mutable reference: data.add("c"); // legal in Java, mutates the shared object ``` Kotlin marked the parameter read-only, but **Java cannot see that annotation-level intent** — it just sees a `java.util.List` with `add`, `remove`, `set`, `clear`. ## Two outcomes depending on the backing object 1. **Mutable backing** (`mutableListOf`, `ArrayList`, etc.): Java's `add` succeeds and mutates the shared object. Your Kotlin code, holding the same reference as a read-only `List`, **observes the new element** via aliasing — a surprising 'my read-only list grew' bug. 2. **Immutable/unmodifiable backing** (`listOf(...)` passed across, or a `Collections.unmodifiableList` copy): Java's `add` throws **`UnsupportedOperationException`** at runtime. You often cannot predict which, because the caller controls the backing. ## How to protect yourself - **Defensive copy on entry** when you need a stable snapshot: ```kotlin fun process(items: List<String>) { val snapshot = items.toList() // independent copy; external mutation can't affect it // work with snapshot } ``` - Document that callers must not retain/mutate the passed collection, or accept a copy. - For genuinely immutable contracts across boundaries, use `kotlinx.collections.immutable` types, which remain immutable even to Java callers (their mutators throw). ## Keywords/APIs `kotlin.collections.List` -> `java.util.List`, type erasure of the read-only/mutable split, aliasing, `toList()` defensive copy, `UnsupportedOperationException`, `Collections.unmodifiableList`, kotlinx persistent collections.
- If you pass a listOf(...) result to a Java method that calls add(), what happens?It depends on the backing; listOf often yields an effectively unmodifiable structure, so add throws UnsupportedOperationException at runtime.
- How do you guarantee a parameter list cannot be mutated by anyone, even Java?Take a defensive copy with toList() inside your function, or use a kotlinx.collections.immutable PersistentList whose mutators throw for any caller.
Kotlin's read-only label is like a 'staff only' sticker on a door that has no actual lock — Java walks right through it.
saying these in an interview costs you the question
- Claiming Java cannot mutate a Kotlin read-only List parameter
- Believing the read-only marker survives to the JVM/Java side
- Not anticipating aliasing-visible mutation in Kotlin code
- Assuming a copy is made automatically at the boundary