skip to content

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?

level: seniorimportance: should knowfreq 35%

answer

  1. List<String> from Java -> List<String!>!
  2. Container AND elements both platform
  3. for-loop can NPE on null list or null element
  4. orEmpty() guards container, filterNotNull() guards elements
  5. Destructuring/indexing carry the same trap

basics

~20 s

Both 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 s

When 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
kotlin
// 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 NPE

go deeper

for a junior

Recognizes that a Java collection might be null and should be guarded before iterating.

for a middle

Knows both container and elements are platform types and uses orEmpty()/filterNotNull().

for a senior

Explains exactly where each NPE can fire (for, element binding, indexing, destructuring) and pins both layers per contract.

for a principal

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

context