How do platform types behave with Java generics and collections — e.g. a Java method returning List<String> — and what subtle NPE traps appear when iterating or destructuring such results in Kotlin?
answer
- List<String> from Java -> List<String!>!
- Container AND elements both platform
- for-loop can NPE on null list or null element
- orEmpty() guards container, filterNotNull() guards elements
- Destructuring/indexing carry the same trap
basics
~20 sBoth the collection and the things inside it become platform types. So a Java List<String> can be null, and any element can be null too, even though Kotlin lets you treat them as non-null. Iterating may NPE on a null element.
solid answer
~50 sWhen a Java method returns `List<String>` with no annotations, Kotlin sees `(Mutable)List<String!>!` — the **container** is a platform type (could be null) and each **element** is a platform type too. Kotlin lets you loop with `for (s in list)` and treat each `s` as non-null `String`, but if the list contains a null, the per-element intrinsic check throws an NPE during iteration. Worse, the whole `list` reference may be null, so the `for` itself can NPE. Mitigation: pin the container (`val list: List<String>? = ...`) and the elements according to the real contract; use `filterNotNull()` if elements may be null, and guard the container with `?:` `emptyList()`. Also watch destructuring (`val (a, b) = pair`) and indexed access `list[0]`, which carry the same hidden-null risk. Annotated collections (e.g. `List<@Nullable String>`) resolve these to proper Kotlin nullable types.
code
kotlin · 12 lines// Java: List<String> getTags() (unannotated)
val tags = javaObj.tags // MutableList<String!>!
for (t in tags) { // NPE if tags == null
println(t.length) // NPE if t == null
}
// Safe:
val clean: List<String> = javaObj.tags
.orEmpty() // null list -> []
.filterNotNull() // drop null elements
clean.forEach { println(it.length) } // no NPEgo deeper
Recognizes that a Java collection might be null and should be guarded before iterating.
Knows both container and elements are platform types and uses orEmpty()/filterNotNull().
Explains exactly where each NPE can fire (for, element binding, indexing, destructuring) and pins both layers per contract.
Mandates annotated/wrapped boundaries for collection-returning Java APIs and reasons about how platform recursion interacts with generics and mapped types across the codebase.
## Platform types are recursive over generics For an unannotated Java `List<String> getTags()`, Kotlin infers: ``` MutableList<String!>! ``` Two independent platform layers: 1. **The container** `...!` — the list reference itself may be null. 2. **The element** `String!` — each element may be null. Kotlin relaxes null checks for **both** layers, so the code below compiles with no warning: ```kotlin val tags = javaObj.tags // MutableList<String!>! for (t in tags) { // (1) NPE here if tags itself is null println(t.length) // (2) NPE here if an element is null } ``` ## Where the NPEs hide - **Container null**: the `for`/iterator call dereferences `tags`; null → NPE before the loop body. - **Element null**: binding `t` to non-null `String` inserts a per-element intrinsic check; a null element → NPE mid-iteration. - **Indexed access**: `tags[0]` is `String!`; pinning to non-null and finding null throws. - **Destructuring**: `val (a, b) = somePair` from a Java `Pair` invokes `component1()/component2()` returning platform types — same hidden-null trap. ## Making it safe Pin both layers explicitly and handle them: ```kotlin val tags: List<String>? = javaObj.tags // container nullable for (t in tags.orEmpty()) { // orEmpty() avoids container NPE // if elements can really be null, pin element nullable instead: } // if elements may be null: val clean: List<String> = javaObj.tags .orEmpty() // container guard .filterNotNull() // drop null elements ``` - `orEmpty()` turns a null `List<T>?` into an empty list. - `filterNotNull()` removes null elements and returns `List<T>` (non-null elements). - For maps, `Map.get` is a classic: pin the result `T?`. ## Annotated generics resolve cleanly If the Java declares `List<@Nullable String>` (JSR-305 / JSpecify-style), Kotlin maps it to `List<String?>`, and `@NotNull List<String>` to a non-null container — the platform ambiguity disappears. (Honoring those annotations is the sibling concern; here the point is that **unannotated** generics stay platform at every level.) ## Summary - Platform-ness applies **recursively**: container and elements each independently `!`. - NPEs can fire on the `for`, on element binding, on indexing, on destructuring. - Defend with explicit pinning plus `orEmpty()` / `filterNotNull()`.
- Why can `for (t in javaList)` throw even before the loop body runs?The container itself is a platform type and may be null. The `for` calls `iterator()` on it; if the reference is null, that dereference throws an NPE before any element is touched.
- How does an explicit `List<@Nullable String>` annotation on the Java side change the Kotlin type?It removes the element-level platform ambiguity: Kotlin maps it to `List<String?>`, so the compiler enforces null handling on elements rather than relaxing it.
saying these in an interview costs you the question
- Assuming only the container, not elements, can be null
- Thinking a Kotlin for-loop is immune to NPE over a Java list
- Not knowing orEmpty()/filterNotNull() as boundary guards
- Believing platform-ness stops at the top level of a generic type