skip to content

When a Java method returns an unannotated value, how do you choose between pinning it to a non-null T versus a nullable T? in Kotlin, and what does each choice generate at the bytecode level?

level: middleimportance: must knowfreq 55%

answer

  1. Pin = give T! an explicit T or T?
  2. Non-null pin -> Intrinsics.checkNotNullExpressionValue, fail-fast
  3. Nullable pin -> no check, handle with ?./?:
  4. Leaving it platform = no protection, NPE leaks
  5. Choose from real Java contract, not convenience

basics

~20 s

Look at what the Java method really does. If it can return null, store it as T? and use safe calls. If it never returns null, store it as T; Kotlin then adds a check that crashes immediately at the boundary if you were wrong.

solid answer

~50 s

Pinning is the act of giving a platform type an explicit Kotlin type at the call site. Choose based on the real Java contract (docs, source, observed behavior), not on convenience. Pin to T? when null is possible, then handle it with `?.`, `?:`, or a smart-cast `if (x != null)`. Pin to non-null T only when you're confident the value is never null. The difference shows in bytecode: assigning a platform type to a non-null variable makes the compiler insert an **intrinsic null check** — `Intrinsics.checkNotNullExpressionValue(...)` — so a hidden null throws a clear NPE *at the boundary*. Pinning to T? inserts no such check; the value is allowed to be null and the type system carries that forward. So non-null pinning trades a possible early fail-fast for ergonomics, while nullable pinning is conservative and shifts handling to you.

code

kotlin · 10 lines
kotlin
// Java: String getName() (unannotated, may return null)

val a: String = javaObj.name    // compiler inserts checkNotNullExpressionValue
                                // -> NPE here if null (fail-fast at boundary)

val b: String? = javaObj.name   // no check; b may be null
val len = b?.length ?: 0        // safe handling downstream

val c = javaObj.name            // stays String! (platform)
c.length                        // NPE can surface here, far from the read

go deeper

for a junior

Knows you should store a possibly-null Java value as T? and use a safe call.

for a middle

Chooses T vs T? from the real contract and knows non-null pinning inserts a fail-fast intrinsic check.

for a senior

Names checkNotNullExpressionValue, explains why an inferred platform type is the worst case, and standardizes annotating return/property types at the boundary.

for a principal

Drives a boundary policy (annotate once, prefer T?) and weighs fail-fast vs propagation across modules and team debugging cost.

## The decision "Pinning" = assigning the platform type `T!` an explicit Kotlin type. Two options: - **`T` (non-null)** — you assert the value is never null. - **`T?` (nullable)** — you treat null as a real possibility. The right choice is driven by the **actual Java contract**: read the Javadoc, the source, or annotations on related members; consider whether the method documents returning null (e.g. `Map.get`, `getParameter`). Do **not** pick non-null just because it's less typing. ## What each generates ### Pinning to non-null T ```kotlin val name: String = javaObj.name // name is String! ``` The compiler emits an **intrinsic null check**. In modern Kotlin (2.x) that's `Intrinsics.checkNotNullExpressionValue(javaObj.getName(), "javaObj.getName()")`. If the Java method returns null, this throws a `NullPointerException` **immediately at the assignment**, with a message naming the expression — fail-fast at the boundary, easy to debug. ### Pinning to nullable T? ```kotlin val name: String? = javaObj.name ``` **No** intrinsic check is inserted. The value may legitimately be null, and the type system forces you to handle it downstream with: - `?.` safe call: `name?.length` - `?:` elvis: `name ?: "unknown"` - smart cast: `if (name != null) { name.length }` ### Doing nothing (leaving it platform) ```kotlin val name = javaObj.name // inferred as String! (platform) name.length // no check inserted at read; NPE risk persists ``` Letting inference keep it platform means *neither* a fail-fast check *nor* type-system protection — the worst of both: the NPE can surface far away at any use site. ## Practical guidance - Default to **T?** at unfamiliar boundaries; loosen to T only with evidence. - For widely-known nullable APIs (`Map.get`, `HashMap.get`), always T?. - Explicit annotation beats inference: write the type on the variable/property/return so the platform type is resolved once, at the boundary, instead of leaking. ## Summary table | Pin to | Bytecode | If Java value is null | You must | |--------|----------|-----------------------|----------| | `T` | intrinsic null check | NPE at boundary (fail-fast) | nothing extra | | `T?` | no check | value is null, no crash | handle with ?./?:/smart cast | | (left platform) | no check | NPE later at use | nothing forces handling |

  • Why is leaving a value as an inferred platform type worse than pinning to non-null?
    Non-null pinning at least inserts a fail-fast intrinsic check at the boundary. An inferred platform type inserts nothing, so a null can travel and crash at an arbitrary later use site, which is much harder to debug.
  • Name a common Java API where you should always pin to T?.
    Map.get / HashMap.get returns null for a missing key, so the Kotlin call site should be T?.

saying these in an interview costs you the question

  • Always pinning to non-null to avoid safe calls
  • Believing T? insertion adds a runtime null check (it doesn't)
  • Not knowing the compiler inserts an intrinsic check on non-null assignment
  • Choosing nullability by convenience rather than the Java contract

context