When a Kotlin class overrides a Java method whose parameter or return is a platform type, how do you choose the signature, and what team-level strategy hardens an entire Java boundary against null holes?
answer
- Override of String! -> pick nullable or non-null
- Inherited params: default nullable (Java may pass null)
- Returns: tighten to non-null if guaranteed
- Facade third-party + annotate owned Java
- Fail fast with requireNotNull at the edge
basics
~20 sWhen you override a Java method, Kotlin lets you pick nullable or non-null for the platform-typed parts. Choose nullable if callers might pass null. As a team, annotate Java, wrap third-party APIs, and turn unknown values nullable at the edge.
solid answer
~50 sOverriding a Java method that uses platform types forces an explicit choice: for each platform-typed parameter/return you must commit to a nullable or non-null Kotlin type, and Kotlin accepts either as a valid override. The safe default for **parameters** of an inherited contract is nullable (`override fun handle(e: Event?)`), because Java callers can legally pass null and a non-null parameter only inserts an intrinsic check that throws — moving the failure into your method. For **returns** you may tighten to non-null if you guarantee it. At team scale: (1) annotate owned Java with `@NotNull`/`@Nullable` or package-level JSR-305 defaults; (2) facade third-party Java behind a thin Kotlin adapter that fixes nullability once; (3) set `-Xjsr305=strict`; (4) make the boundary fail fast with `requireNotNull`/`checkNotNull` so a stray null surfaces with a message at the edge, not as a deep NPE; (5) consider Kotlin's runtime intrinsics and stack-trace clarity when deciding where to assert.
code
kotlin · 7 lines// Java: interface Listener { void onEvent(Event e); }
class SafeHandler : Listener {
override fun onEvent(e: Event?) { // nullable param = safe override
val event = requireNotNull(e) { "null event from source" }
process(event) // fails fast at the edge
}
}go deeper
Knows you must pick nullable or non-null when overriding a Java method and that nullable params are often safer.
Explains why a non-null inherited parameter merely relocates the NPE and uses requireNotNull to guard.
Justifies parameter-nullable / return-tighten heuristics and recovers safety via annotations on owned Java.
Designs the boundary as a seam: facade third-party APIs, enforce -Xjsr305, centralize conversion, and make failures local and diagnosable.
## Overriding across the boundary When a Kotlin class extends/implements a Java type, each platform-typed parameter or return in the inherited signature becomes a **choice point**. Kotlin requires you to write a concrete Kotlin type and accepts both nullable and non-null as a valid override of `String!`. ```kotlin // Java interface: void onEvent(Event e); // platform type Event! class MyHandler : Listener { override fun onEvent(e: Event?) { ... } // nullable: safe — Java may pass null // or override fun onEvent(e: Event) { ... } // non-null: Kotlin inserts a check; // a null arg throws inside your method } ``` ### Choosing parameter nullability For **parameters of an inherited contract**, prefer **nullable** unless the Java contract documents non-null. The reason: the caller is Java code you may not control; it can legally pass `null`. A non-null Kotlin parameter does not prevent that — it merely adds an intrinsic null check that throws `NullPointerException` on entry, relocating the failure into your override. Declaring the parameter nullable lets you handle the case explicitly. ### Choosing return nullability For **returns**, you may **tighten to non-null** when you can guarantee it (you produce the value). Returning `String?` when callers expect non-null pushes the burden downstream. ## Team-level strategy to harden a whole boundary Individual fixes don't scale; treat the Java edge as an architectural seam: 1. **Annotate owned Java** with `@NotNull`/`@Nullable`, or apply package-wide JSR-305 defaults (`@ParametersAreNonnullByDefault` + custom `@TypeQualifierDefault`). This restores checking at *every* Kotlin call site at once. 2. **Facade third-party Java** behind a thin Kotlin adapter (a single file) that converts platform types to explicit Kotlin types and validates them once. Downstream Kotlin then sees only real types. 3. **Set `-Xjsr305=strict`** (or `warn` during migration) so annotations are enforced, and gate the flag in CI. 4. **Fail fast at the edge** with `requireNotNull(x) { "..." }` / `checkNotNull` so a violated assumption produces a clear, located exception instead of a distant NPE. This is about **diagnosability**, not just correctness. 5. **Mind the intrinsics**: Kotlin emits `Intrinsics.checkNotNull*` calls; understand that a non-null Kotlin type at the boundary is a *runtime* assertion, not a compile-time guarantee, when the value originates in Java. ## The principle Null holes are a property of the **boundary**, not of individual call sites. Centralize the conversion (annotate or facade), choose override signatures by who can pass null, and make every assumption fail loudly and locally.
- Why is a non-null parameter type on an overridden Java method not actually 'safer'?Java callers can still pass null; the non-null type only inserts a runtime intrinsic check that throws inside your method. It doesn't prevent the null — it just relocates and obscures the failure.
- What's the advantage of a Kotlin facade over annotating each call site?The facade fixes nullability once, in one place; all downstream Kotlin sees real types, and third-party Java you can't edit is handled without scattering guards.
The Java boundary is a customs checkpoint: inspect and stamp each value once at the border (facade/annotate), rather than re-checking passports deep inside the country.
saying these in an interview costs you the question
- Claiming a non-null Kotlin parameter stops Java from passing null
- Always tightening inherited parameters to non-null
- Treating null holes as per-call-site bugs rather than a boundary concern
- Ignoring `-Xjsr305` enforcement in the build
- Letting a stray null surface as a deep NPE instead of failing fast at the edge