skip to content

You declare `fun process(items: List<String>)` in Kotlin and call it from Java. Can the Java caller add elements to that list? Explain.

level: seniorimportance: should knowfreq 45%

answer

  1. Read-only List compiles to java.util.List
  2. Java sees add/remove regardless of Kotlin intent
  3. Mutation visible to Kotlin via aliasing
  4. Immutable backing -> UnsupportedOperationException
  5. Defensive copy with toList() at the boundary

basics

~10 s

Yes. 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 s

Yes. 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
// 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

for a junior

May guess wrongly; can state that List has no add in Kotlin but unsure about the Java view.

for a middle

Knows Java sees java.util.List and can mutate, naming UnsupportedOperationException for immutable backings.

for a senior

Explains the JVM erasure of the split, aliasing visibility, and defensively copies at boundaries.

for a principal

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

context