skip to content

When a Java method returns java.util.List<String>, what Kotlin type does it appear as, and what are the safety implications of that mapping?

level: middleimportance: must knowfreq 65%

answer

  1. Java List -> MutableList<String!> by default
  2. Mutable because Java has no read-only split
  3. add may throw UnsupportedOperationException
  4. Copy defensively: toList()/toMutableList()
  5. @Unmodifiable / nullability annotations tighten inference

basics

~20 s

It usually shows up as a MutableList, so Kotlin lets you add and remove from it. But Java may not expect that list to change, so calling add could throw or corrupt the caller's data.

solid answer

~40 s

Kotlin maps java.util.List to a single underlying type but presents it to your Kotlin code as MutableList<String!> by default — both because Java has no read-only/mutable split and because the element type is a platform type (String!, unknown nullability). So you can call add/remove on it directly, with no compile-time barrier. The risk: the Java side may have returned an unmodifiable list (Collections.unmodifiableList, List.of) or a list it relies on staying constant. Mutating it can throw UnsupportedOperationException at runtime or silently break the Java caller's invariants. Treat Java-returned collections as untrusted: copy defensively with toList()/toMutableList(), or annotate the Java API with @Unmodifiable / use proper @NotNull-style annotations so Kotlin infers a tighter type.

code

kotlin · 5 lines
kotlin
// Java returns Collections.unmodifiableList(internal)
val names = javaApi.names          // MutableList<String!>
// names.add("x")  // compiles, but throws UnsupportedOperationException at runtime
val safe = names.toMutableList()    // defensive copy; safe to mutate
safe.add("x")

go deeper

for a junior

Knows a Java List can be modified from Kotlin but may be unsure why or what breaks.

for a middle

Explains the MutableList<String!> default, names UnsupportedOperationException, and copies defensively.

for a senior

Discusses mapped types, platform types, and uses @Unmodifiable/nullability annotations to harden the boundary.

for a principal

Sets team conventions for crossing Java/Kotlin collection boundaries (always copy or annotate) and reasons about mutation-leak bugs at scale.

## How Java collections map into Kotlin Kotlin maps several JDK collection types onto its own declarations through **mapped types**. `java.util.List`, `java.util.Set`, `java.util.Map`, `java.util.Collection`, and `java.util.Iterator` each map onto a Kotlin interface. Crucially, each Java type corresponds to **both** a read-only and a mutable Kotlin view — and at a Java boundary the compiler defaults to the **mutable** one. So a Java signature `List<String> getNames()` is seen from Kotlin as roughly `(Mutable)List<String!>`: - **`Mutable`** — because Java's `List` is a read/write interface with no read-only counterpart, Kotlin assumes it may be mutated, exposing `add`, `remove`, `set`. - **`String!`** — a **platform type**: nullability is unknown, so Kotlin neither forces a null check nor guarantees non-null. ```kotlin // Java: List<String> getNames() val names = api.names // inferred MutableList<String!> names.add("extra") // compiles — no read-only barrier ``` ## Why this is dangerous The Java method might return: - A list wrapped in `Collections.unmodifiableList(...)` or created with `List.of(...)` / `Arrays.asList(...)`. Calling `add`/`remove` throws **`UnsupportedOperationException`** at runtime. - A list the Java code holds internally and assumes won't change. Mutating it from Kotlin **silently corrupts** the Java side's state. Kotlin gives you **no compile-time warning** here, because Java exposes no read-only contract for the compiler to honor. ## Defensive strategies 1. **Copy on receipt:** `val safe = api.names.toList()` (read-only snapshot) or `.toMutableList()` (your own writable copy). This decouples you from the Java object entirely. 2. **Declare the intent in Kotlin:** assign to an explicit `List<String>` reference so *your* code can't mutate it accidentally: `val names: List<String> = api.names`. 3. **Annotate the Java API:** annotations like JetBrains `@Unmodifiable` (on the return), or nullability annotations (`@NotNull`/`@Nullable`), let the Kotlin compiler infer a read-only and/or non-null type instead of the permissive default. ## Keywords/APIs `toList()`, `toMutableList()`, mapped types, platform type (`String!`), `Collections.unmodifiableList`, `List.of`, `@Unmodifiable`, `UnsupportedOperationException`.

  • Why doesn't the Kotlin compiler stop you from calling add() on a Java-returned list?
    Because Java's List interface has no read-only/mutable distinction, so Kotlin defaults to the permissive MutableList view; the compiler has no information that the list is unmodifiable.
  • How can a Java library author make Kotlin treat the return as read-only?
    Annotate the return type with @Unmodifiable (JetBrains annotations); Kotlin then infers the read-only List view instead of MutableList.

saying these in an interview costs you the question

  • Assuming Java lists always arrive as read-only List
  • Mutating a Java-returned collection without copying
  • Not knowing UnsupportedOperationException is a runtime risk
  • Confusing the mutability default with the platform-type nullability issue

context