skip to content

Contrast how Kotlin imports a Java method that is un-annotated versus one annotated @NotNull. What changes at compile time and at runtime?

level: middleimportance: must knowfreq 60%

answer

  1. Platform type T! = flexible, no checks
  2. @NotNull -> strict T, compile-time non-null
  3. Kotlin trusts annotation, inserts intrinsic check
  4. Contract violation -> fast NPE at boundary
  5. Platform type defers; @NotNull fails fast + clear

basics

~10 s

Un-annotated, Kotlin uses a flexible platform type and skips null checks, so a hidden null blows up later. With @NotNull, Kotlin imports a strict non-null type and trusts it, checking nullability at compile time.

solid answer

~50 s

An un-annotated Java declaration becomes a platform type (T!): Kotlin lets you assign it to either T or T?, and applies no compile-time null enforcement; a secretly-null value used as non-null throws an NPE at the dereference. With @NotNull, Kotlin imports the strict non-null T, so the compiler treats it as guaranteed and you can assign it straight into a non-null variable. The trust is one-directional: Kotlin believes the annotation, so it may insert an *intrinsic* null check (checkNotNull) right after the call boundary; if Java violates the contract and returns null, you get an immediate IllegalStateException/NPE at the boundary rather than a delayed NPE deep in your code. So the practical difference is: platform type defers errors and offers no compile-time help, whereas @NotNull gives compile-time null information and fails fast and clearly at the boundary if the contract is broken.

code

kotlin · 8 lines
kotlin
// Java A: String  raw();        // platform: String!
// Java B: @NotNull String safe(); // strict: String

fun demo(a: A, b: B) {
    val x: String = a.raw()   // compiles; NPE at THIS line if null
    val y: String = b.safe()  // compiles; intrinsic guards the boundary
    println(x.length + y.length)
}

go deeper

for a junior

Knows un-annotated is risky and @NotNull is treated as non-null.

for a middle

Clearly separates compile-time (platform = relaxed, @NotNull = enforced) from runtime, and knows non-null widens to nullable.

for a senior

Explains intrinsic checks, fail-fast-at-boundary semantics, and the soundness trade-off behind platform types.

for a principal

Reasons about when to annotate Java sources, the diagnostic-quality benefit of boundary intrinsics, and performance/binary considerations of intrinsic checks across a large interop surface.

## Two import modes Kotlin imports a Java member in one of two ways depending on annotations. ### 1. Un-annotated -> platform type `T!` A platform type is a **flexible type**: it is simultaneously usable as `T` and `T?`. The compiler imposes **no** null checks. ```kotlin // Java: String find(); // no annotation val r = obj.find() // inferred platform type String! val a: String = r // allowed val b: String? = r // also allowed println(r.length) // compiles; NPE here at runtime if r was null ``` This is a pragmatic compromise: forcing every un-annotated Java return to be `T?` would make Java interop unbearable, while forcing `T` would be unsound. Platform types let *you* choose, putting the risk on you. ### 2. `@NotNull` -> strict `T` ```kotlin // Java: @NotNull String find(); val r = obj.find() // inferred String (non-null) val a: String = r // OK println(r.length) // OK, no ?. needed ``` ## Runtime: the trust is enforced defensively Kotlin **trusts** `@NotNull` at compile time but doesn't assume Java is honest at runtime. When a `@NotNull` Java value flows into Kotlin non-null territory (parameters, fields read into non-null vars), the compiler may emit an **intrinsic check** (`Intrinsics.checkNotNull` / `checkNotNullExpressionValue`). If Java actually returns `null`, you get an immediate, clearly-located `NullPointerException` *at the boundary* — not a confusing one three call frames later. | | Un-annotated (`T!`) | `@NotNull` (`T`) | |---|---|---| | Compile-time null check | None (relaxed) | Yes (treated non-null) | | Assignable to `T` | Yes (your risk) | Yes (safe) | | Assignable to `T?` | Yes | Yes (widening) | | If value is secretly null | NPE at first unsafe deref | Fast NPE at the boundary intrinsic | ## Why it matters Annotating Java APIs (or using them) converts a quiet, deferred failure mode into early compile-time information plus a fail-fast runtime guard — a strictly better interop boundary.

  • Where does the NPE surface for a platform type that turns out to be null versus a @NotNull that lies?
    Platform type: at the first place you dereference it as non-null. @NotNull violation: at the boundary intrinsic check immediately after the call, giving a clearer stack.
  • Can you choose to treat a @NotNull Java result as nullable in Kotlin?
    Yes — non-null widens to nullable, so `val s: String? = b.safe()` compiles. The annotation tightens the lower bound to non-null but doesn't forbid storing it in a nullable variable.

saying these in an interview costs you the question

  • Claiming platform types are the same as nullable types
  • Saying @NotNull does nothing at runtime ever
  • Asserting Kotlin forbids assigning a non-null to a nullable variable
  • Thinking un-annotated Java forces you to use ?. everywhere
  • Believing the compiler checks the Java method body for honesty

context