skip to content

Honoring @Nullable/@NotNull

When a Java API is annotated with @Nullable or @NotNull, Kotlin maps it to T? or T and the platform type disappears. Knowing which annotation families are honored, and how package-level defaults work, is what makes interop safe rather than hopeful.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

4

When you call a Java method from Kotlin, how do @Nullable and @NotNull annotations on that Java method change the type Kotlin sees?

level: juniorimportance: must knowfreq 70%

answer

  1. No annotation -> platform type T! (relaxed)
  2. @NotNull -> T (non-null)
  3. @Nullable -> T? (forced handling)
  4. Annotation removes the platform type
  5. JetBrains / JSR-305 / Android / JSpecify recognized

basics

~20 s

If the Java method is marked @NotNull, Kotlin treats the result as a normal non-null type. If it is @Nullable, Kotlin treats it as nullable and makes you check for null. Without an annotation, Kotlin can't tell.

solid answer

~40 s

Java types have no null information, so by default Kotlin imports them as platform types (written T!), which skip null checks. When the Java declaration carries a recognized nullability annotation, Kotlin removes the platform type and substitutes a real Kotlin type: @NotNull becomes the non-null T, and @Nullable becomes the nullable T?. So a @NotNull String returns Kotlin's String (no ?. needed and assignable to a non-null variable), while a @Nullable String returns String? and the compiler forces you to handle null via ?., ?:, or a smart cast. Recognized families include JetBrains org.jetbrains.annotations, JSR-305 javax.annotation, Android androidx.annotation, Checker Framework, Eclipse, and others. This turns silent Java NPEs into compile-time guarantees at the interop boundary.

code

kotlin · 9 lines
kotlin
// Java side:
// public @NotNull  String safe()    { return "x"; }
// public @Nullable String maybe()   { return null; }

fun use(j: Demo) {
    val a: String  = j.safe()          // @NotNull -> String, no check
    val b: String? = j.maybe()         // @Nullable -> String?
    println(b?.length ?: 0)            // compiler requires null handling
}

go deeper

for a junior

Knows @NotNull -> String and @Nullable -> String?, and that you must null-check the nullable one.

for a middle

Explains platform types as the un-annotated default and that the annotation removes them; names a couple of annotation families.

for a senior

Discusses that @NotNull is a trusted contract not a runtime guarantee, lists multiple recognized families, and the runtime-NPE consequences when the contract is violated.

for a principal

Frames it as a policy decision for library interop boundaries, weighs annotating Java sources vs. JSpecify module-level defaults vs. wrapping at the boundary, and reasons about intrinsic null checks and binary-compatibility implications.

## The problem: Java doesn't track nullability In Java, any reference (`String`, `List<T>`, etc.) can be `null`, and the type system records nothing about whether a given value may be null. When Kotlin calls Java, it therefore can't know if a returned `String` is safe. ## Platform types (the default) For an *un-annotated* Java declaration, Kotlin assigns a **platform type**, written `String!` (you can't write `!` yourself; it's compiler-internal). Platform types **relax** null checking: you may use the value as either `String` or `String?`, and if you treat a secretly-null value as non-null you get a runtime `NullPointerException` exactly where you'd get one in Java. This is a pragmatic escape hatch, not a guarantee. ## What nullability annotations do When the Java declaration is annotated with a **recognized** nullability annotation, Kotlin *removes the platform type* and imports a precise Kotlin type: - `@NotNull String foo()` -> Kotlin sees `String` (non-null). Assignable to a non-null `val s: String`, no null check needed. - `@Nullable String foo()` -> Kotlin sees `String?` (nullable). The compiler **forces** safe handling (`?.`, `?:`, `!!`, or a null check + smart cast). ```kotlin // Java: @NotNull String name(); @Nullable String nick(); val n: String = javaObj.name() // OK: @NotNull -> String val k: String? = javaObj.nick() // @Nullable -> String? val bad: String = javaObj.nick() // COMPILE ERROR: String? is not String println(javaObj.nick().length) // COMPILE ERROR: must use ?. println(javaObj.nick()?.length) // OK ``` ## Recognized annotation families Kotlin understands many widely-used annotations, including: - **JetBrains**: `org.jetbrains.annotations.Nullable` / `NotNull` - **JSR-305**: `javax.annotation.Nullable` / `Nonnull` - **Android**: `androidx.annotation.Nullable` / `NonNull` - **JSpecify**: `org.jspecify.annotations.Nullable` / `NonNull` - Checker Framework, Eclipse JDT, Lombok, FindBugs/SpotBugs, RxJava, and others. It matches by the **simple name** (`Nullable`/`NotNull`/`NonNull`/`Nonnull`/`CheckForNull`), so several vendor packages are honored. ## Why it matters Annotations convert an interop boundary that would silently throw NPEs into one where the Kotlin compiler enforces null handling — pushing failures from runtime to compile time.

  • If a Java method has no nullability annotation, what type does Kotlin assign and what is the risk?
    A platform type (T!). Kotlin lets you use it as non-null or nullable; if it's actually null and you used it as non-null, you get a runtime NPE at that point — null checking is relaxed, not enforced.
  • Does adding @NotNull guarantee the value can never be null at runtime?
    No. It's a contract Kotlin trusts at compile time. If the Java code lies and returns null, you can still get a runtime NPE; Kotlin may insert an intrinsic null check that throws.

An un-annotated Java value is a parcel with no label — Kotlin lets it through but you carry the risk; the annotation is the 'fragile / contains-null' sticker that makes the compiler insist you handle it.

saying these in an interview costs you the question

  • Saying Java types always import as nullable T? by default
  • Thinking @NotNull provides a runtime guarantee rather than a compile-time contract
  • Confusing platform types with nullable types
  • Believing Kotlin only recognizes JetBrains annotations
  • Claiming the annotations affect Java code's own behavior

context

open as a page

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%

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.

open as a page

What does @ParametersAreNonnullByDefault (and JSR-305 default annotations) do for Kotlin interop, and how would you override it for one nullable parameter?

level: seniorimportance: should knowfreq 40%

basics

~20 s

It tells Kotlin that, unless marked otherwise, every parameter in that scope is non-null, so you don't have to annotate each one. To make a single parameter nullable, you add @Nullable to just that parameter.

open as a page

How do nullability annotations interact with Java generics and array element types when imported into Kotlin, and what are the common gotchas?

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

Annotations can mark not just the outer type but also the element type inside a generic or array. If only the container is marked, the elements may still be platform types, so a list can be non-null while its items aren't checked.

open as a page