skip to content

Beyond simple return values, where else do platform-type null holes hide — for example in Java collections, generics, and arrays — and how do they bite?

level: seniorimportance: should knowfreq 45%

answer

  1. List<String> -> (Mutable)List<String!>!
  2. Null hides in elements/map values/array slots
  3. Per-element intrinsic check -> mid-loop NPE
  4. filterNotNull() at the boundary
  5. Annotate type arg: List<@NotNull String>

basics

~20 s

A null can also hide inside a Java list, map, or array element, or in a generic type argument. Kotlin trusts the whole container as non-null, so the crash pops up only when you read the null element.

solid answer

~40 s

Platform types are not limited to top-level returns; they nest. A Java `List<String>` arrives in Kotlin as `(Mutable)List<String!>!` — both the container and each element are platform types. Iterating with a non-null element type (`for (s: String in javaList)`) inserts a per-element intrinsic null check, so a single null element throws `NullPointerException` mid-loop. The same applies to `Map<K!, V!>!` values, and to Java arrays (`String[]` -> `Array<String!>!`). Generics are especially sticky because the element nullability is invisible at the call site. Guards: declare the element type nullable (`List<String?>`), filter with `filterNotNull()`, map-read defensively (`map[k] ?: ...`), or annotate the Java generic with `@NotNull` on the type argument (e.g. `List<@NotNull String>`). For arrays, copy/validate on entry. The principle is the same as for scalars — make element nullability explicit at the boundary.

code

kotlin · 4 lines
kotlin
// Java: List<String> tags();  Map<String,String> meta();
val tags: List<String?> = repo.tags()
for (t in tags.filterNotNull()) println(t.length)   // null-safe
val v = repo.meta()["k"] ?: "default"               // defensive map read

go deeper

for a junior

Recognizes that a Java list element can be null and cause a crash when read.

for a middle

Describes List<String!>! structure and uses filterNotNull()/defensive map reads to guard.

for a senior

Explains per-element intrinsic checks, array/map/generic cases, and use-site @NotNull on type arguments.

for a principal

Defines boundary-hardening conventions (commit each layer, facade collections) and reviews generics for hidden element nullability.

## Platform types are recursive A platform type isn't just a property of a single returned reference; it propagates through container and generic structure. ### Collections A Java `List<String>` becomes the Kotlin platform type written `(Mutable)List<String!>!`: - the **list itself** is a platform type (could be null), and - **each element** is a platform type (could be null). ```kotlin // Java: List<String> tags(); // may contain null entries for (t: String in repo.tags()) { // element typed non-null println(t.length) // NPE on the first null element } ``` The loop compiles cleanly. Kotlin inserts an intrinsic null check per element, so the failure surfaces only when a null element is read. ### Maps `Map<K, V>` -> `Map<K!, V!>!`. A `map[key]` Kotlin lookup already returns `V?` for true Kotlin maps, but for a Java map the **value type itself** is a platform type, so even a present value may be null. Read defensively: ```kotlin val v = javaMap[k] ?: error("missing or null for $k") ``` ### Arrays Java `String[]` -> Kotlin `Array<String!>!`. Element access (`arr[i]`) is a platform type; assigning to a non-null `String` triggers a check on read. ## Why generics make it worse With a scalar return you can eyeball the type at the call site. With `List<String!>` the dangerous nullability is buried in the **type argument**, easy to miss in review, and `!!`-free code still crashes. ## Guarding the boundary - **Declare element nullability explicitly**: `val tags: List<String?> = repo.tags()` then `tags.filterNotNull()`. - **`filterNotNull()`** collapses `List<T?>` to `List<T>` cleanly at the boundary. - **Map reads**: always `?:` the lookup. - **Annotate the Java side on the type argument**: `List<@NotNull String>` (use-site type-use annotations) so Kotlin sees `List<String>`. - **Defensive copy / validation** for arrays you don't control. ## Mental model Treat a Java container as `Container<Element!>!` and **commit each layer** — the container and the element — to an explicit Kotlin nullability before you trust it. The null hole hides one level deeper than people expect.

  • Why does `for (s: String in javaList)` compile even if the list contains nulls?
    Each element is a platform type assignable to non-null `String`; Kotlin defers the check to runtime, inserting a per-element intrinsic that throws NPE only when a null is actually read.
  • What's the cleanest one-liner to go from a Java `List<String!>!` to a guaranteed `List<String>`?
    Declare it as `List<String?>` and call `.filterNotNull()`, which both null-checks and narrows the element type in one step.

A Java collection is a box of unlabeled jars: the box might be missing, and any jar inside might be empty — you must check both layers.

saying these in an interview costs you the question

  • Thinking only the container, not the elements, is a platform type
  • Assuming Kotlin map lookups on Java maps can never be null
  • Believing iteration is safe because there's no `!!`
  • Forgetting type-use annotations can annotate the element type
  • Using `!!` on each element instead of `filterNotNull()`

context