skip to content

What is a Kotlin 'platform type' and why can a value coming from Java code throw a NullPointerException even though your Kotlin code looks fully null-safe?

level: juniorimportance: must knowfreq 75%

answer

  1. String! = nullability unknown
  2. Java has no null info -> platform type
  3. NPE at use, not at assignment-compile
  4. Fix: explicit ? or requireNotNull
  5. @Nullable/@NotNull restore checks

basics

~20 s

When Java returns a value, Kotlin doesn't know if it can be null, so it trusts you. If you treat it as non-null and it actually is null, you only crash later when you use it.

solid answer

~40 s

Java types carry no nullability information, so when a Java method returns something, Kotlin assigns it a 'platform type', shown in the IDE as String! (with an exclamation mark). A platform type relaxes null checks: you may treat it as either String or String?. If you assign it to a non-null Kotlin type and the actual value is null, the compiler does not complain at the assignment; instead it inserts an implicit null-check (intrinsic) that throws NullPointerException at the point of use/assignment. This is the boundary 'hole': Kotlin's normally airtight null safety is suspended for un-annotated Java values. The fix is to explicitly choose a nullable type (val x: String? = javaCall()) or assert/guard with requireNotNull / checkNotNull, so the failure is intentional and located where you expect it.

code

kotlin · 4 lines
kotlin
// Java: String findName() { return null; }
val bad: String = repo.findName()      // compiles; NPE at runtime
val safe: String? = repo.findName()    // explicit nullable
val name = requireNotNull(repo.findName()) { "name missing" } // intentional guard

go deeper

for a junior

Knows Java values become platform types and that a hidden NPE can result; can fix it with ? or a guard.

for a middle

Explains the String! notation, the both-assignable rule, and where the intrinsic null check fires.

for a senior

Discusses honoring @Nullable/@NotNull, choosing explicit boundary types, and why the crash location differs from the source of null.

for a principal

Frames it as a deliberate convenience/safety trade-off and sets team conventions (annotate Java, type the boundary, fail fast with requireNotNull).

## The problem: Java has no nullability in its type system In Kotlin, every type is either non-null (`String`) or nullable (`String?`), and the compiler enforces the difference. Java has no such distinction — a `String` in Java may freely hold `null`. So when Kotlin calls Java code, it cannot know whether a returned reference is nullable. ## Platform types Rather than pessimistically forcing every Java value to be nullable (annoying) or optimistically forcing it non-null (unsafe), Kotlin introduces a **platform type**, written `String!` (you cannot write this notation yourself; it appears in IDE hints and error messages). A platform type means *"nullability unknown — you decide."* For a platform type you may: - assign it to a **non-null** Kotlin type (`val s: String = javaGetName()`), or - assign it to a **nullable** Kotlin type (`val s: String? = javaGetName()`). The compiler permits **both** with no warning. That flexibility is exactly the **null-safety hole**. ## Where the NPE actually happens If you choose the non-null path and the value is really `null`, you do **not** get an error at compile time. Kotlin inserts an **intrinsic null check** (`Intrinsics.checkNotNull` / `checkNotNullExpressionValue`) at the assignment or first dereference, which throws `NullPointerException` — possibly far from the real source of the bad `null`. ```kotlin // Java: class Repo { String findName() { return null; } } val name: String = repo.findName() // platform type String! -> String println(name.length) // NPE thrown here (or at the assignment) ``` The code *looks* null-safe — there is no `!!` anywhere — yet it crashes. ## Guarding the boundary - **Declare it nullable** and handle it: `val name: String? = repo.findName()` then use `?.`, `?:`, `let`, etc. - **Assert intent explicitly** when you truly require non-null: `val name = requireNotNull(repo.findName()) { "name missing" }` or `checkNotNull(...)`. This throws an `IllegalArgumentException`/`IllegalStateException` with a message at the boundary, not a mystery NPE downstream. - **Annotate the Java side** with `@Nullable` / `@NotNull` (JetBrains, JSR-305, Android, Jakarta). Kotlin honors these and turns `String!` into a real `String?` or `String`, restoring compile-time checking. ## Key takeaway Platform types trade compile-time safety for interop convenience. The discipline is: **assign every Java value to an explicit Kotlin nullable type at the boundary**, then narrow deliberately.

  • How does the IDE display a platform type, and why can't you write it in source?
    It shows as `String!` in inferred-type hints and errors. The `!` syntax is compiler/IDE-only because it represents 'either nullable or not'; source code must commit to `String` or `String?`.
  • If you never give the Java value an explicit type, what type does Kotlin pick?
    It keeps the platform type via inference (e.g. `val x = javaCall()` gives `x` a platform type), so the relaxed checking propagates — better to annotate the variable explicitly.

A platform type is like an unlabeled jar from someone else's kitchen: Kotlin lets you assume it's full, but you only find out it's empty when you reach in.

saying these in an interview costs you the question

  • Claiming Kotlin makes all Java values nullable automatically
  • Saying the NPE happens at compile time
  • Thinking `!!` is required for the crash to occur
  • Believing platform types can be written by hand as `String!`
  • Assuming the IDE annotation hints have no runtime effect

context