skip to content

How do !! and platform types interact when calling Java from Kotlin, and why does Java interop drive overuse of !!?

level: seniorimportance: should knowfreq 40%

answer

  1. Java -> Kotlin un-annotated = platform type String!
  2. platform type = use as T or T?, no enforcement
  3. !! at the boundary hides real Java nulls
  4. Kotlin honours @Nullable/@NotNull and JSpecify
  5. type the seam as T? and handle null once

basics

~20 s

Java values whose nullability Kotlin can't see are 'platform types'. Kotlin lets you use them without forcing null checks, so developers reach for !! to convert them to non-null — sometimes hiding real nulls and causing crashes later.

solid answer

~50 s

When Kotlin calls Java code, the compiler often cannot tell whether a returned reference is nullable, so it assigns it a **platform type**, written `String!` in diagnostics. A platform type is permissive: you may treat it as either `String` or `String?` with no compiler enforcement. If you assign it to a non-null Kotlin type and the Java value is actually null, you get an NPE — and people frequently add `!!` to make that conversion explicit or to satisfy the compiler. The hazard is that `!!` (and platform types generally) bypass null safety, so a null sneaking out of Java surfaces as a runtime `NullPointerException`, often far from the call site. The disciplined approach is to read Java nullness annotations (`@Nullable`/`@NotNull`, JSpecify) which Kotlin honours and turn into real `T?`/`T`, and to explicitly type the value as `String?` at the boundary and handle null there, rather than assert it away with `!!`.

code

kotlin · 8 lines
kotlin
// Java (no annotations):  public String findById(long id) { ... may return null ... }

// Risky: platform type asserted non-null; NPE if Java returned null
val user: User = repo.findById(1)!!

// Disciplined: treat the seam as nullable and handle it once
val user: User? = repo.findById(1)
val name = user?.name ?: "anonymous"

go deeper

for a junior

Knows Java calls can return null and that !! can crash, even if 'platform type' is unfamiliar.

for a middle

Defines platform types, recognizes String! in diagnostics, and types the boundary as T? instead of asserting.

for a senior

Explains how Kotlin honours @Nullable/@NotNull/JSpecify and designs the seam to handle null once, keeping !! out of the interior.

for a principal

Drives an interop policy: annotate Java APIs, treat un-annotated seams as nullable, and forbid interior !! through review/lint.

## Platform types: the source of the pressure Kotlin's type system tracks nullability, but **Java's does not** (a bare `String` in Java may or may not be null). When Kotlin calls an un-annotated Java method, it cannot know the intent, so it gives the result a **platform type**, denoted with a single trailing exclamation, e.g. `String!`. You cannot write `String!` yourself; it only appears in compiler messages and inferred types. ```kotlin // Java: public String getName() { ... } val name = javaObj.name // inferred platform type String! ``` A platform type is **relaxed**: the compiler lets you use it as non-null `String` *or* nullable `String?` with no warning. That flexibility is the trap. ## Where !! comes in To move a platform-typed value into a strict non-null Kotlin type, developers often write: ```kotlin val name: String = javaObj.name!! // assert non-null ``` If `getName()` actually returns null, the `!!` (or even the plain assignment to `String`) throws a `NullPointerException`. So Java interop systematically nudges people toward `!!` to 'close the gap' the missing Java nullness leaves — even when the value really can be null. ## The disciplined alternatives 1. **Honour nullness annotations.** Kotlin reads `@Nullable`/`@NotNull` (JetBrains, Android, javax) and **JSpecify** `@Nullable`. An annotated Java method produces a proper `String?` or `String`, eliminating the platform type and the temptation to `!!`. ```kotlin // Java: @Nullable String getName() -> Kotlin sees String? val name: String? = javaObj.name // forced to handle null safely ``` 2. **Type the boundary explicitly.** Annotate the variable as nullable and decide at the seam: ```kotlin val name: String? = javaObj.name val display = name ?: "unknown" ``` 3. **Assert with a message when it truly cannot be null** (documented invariant), via `checkNotNull`/`requireNotNull` rather than bare `!!`. ## Why this matters at scale Platform types let nulls leak across the Java/Kotlin seam silently, and `!!` converts the leak into a crash that is hard to attribute. The right model is: **treat every un-annotated Java boundary as nullable**, handle it once at the seam, and push non-null types inward. That keeps `!!` out of the interior of the codebase entirely.

  • What is the type denoted String! and can you declare a variable of that type yourself?
    It is a platform type from un-annotated Java; you cannot write String! in source — it only appears in inferred/diagnostic output. You pin it by declaring String or String?.
  • How can a Java library author eliminate platform types for Kotlin callers?
    Annotate APIs with @Nullable/@NotNull (or JSpecify @Nullable / @NullMarked); Kotlin then infers proper nullable/non-null types instead of platform types.

A platform type is like an unlabelled package from Java: Kotlin won't force you to check inside, so a null can be hiding in there until you open it.

saying these in an interview costs you the question

  • Doesn't know what a platform type is
  • Thinks Kotlin always knows Java's nullability
  • Routinely !!s every Java call result without considering @Nullable
  • Believes platform types force a null check (they don't)
  • Unaware Kotlin honours JSpecify/@Nullable annotations

context