When a Kotlin class implements a Java interface with unannotated method parameters/returns, what nullability must the override declare, and how can a team make the compiler stricter about platform types?
answer
- Override of unannotated Java method must commit T or T?
- Override signature = hard contract for Kotlin callers
- Java callers ignore your non-null promise; intrinsic check still fires
- -Xjsr305=strict escalates annotated violations to errors
- Annotate Java (@Nullable/@NotNull/@NullMarked) to kill platform types
basics
~20 sWhen you override an unannotated Java method, Kotlin lets you choose whether the types are nullable or not, but the choice becomes a hard contract for your callers. Teams can also turn on a compiler flag to warn whenever a platform type is misused.
solid answer
~40 sIf a Kotlin class overrides a Java interface method whose parameters/return are unannotated (platform), Kotlin requires you to **resolve** the platform types in the override signature — you pick `T` or `T?` for each. That choice is then enforced for your Kotlin callers, so a too-strict non-null parameter can NPE if Java passes null, while a nullable return forces callers to handle null. To get stricter feedback on platform types, a team can enable JSR-305/JSpecify strict mode via `-Xjsr305=strict` (treating annotated Java as proper nullable/non-null and erroring on violations) and use `-Xtype-enhancement-improvements-strict-mode`. Beyond flags, the durable fix is annotating the Java side (`@Nullable`/`@NotNull` or `@NonNullApi` package defaults) so platform types stop appearing. Override design should match what Java actually passes/returns, not the most convenient signature.
code
kotlin · 14 lines// Java: interface Handler { String handle(String input); } // unannotated
class StrictHandler : Handler {
// assert Java never passes/returns null -> non-null contract
override fun handle(input: String): String = input.uppercase()
}
class LenientHandler : Handler {
// model that null is possible on both sides
override fun handle(input: String?): String? = input?.uppercase()
}
// build.gradle.kts:
// kotlin { compilerOptions { freeCompilerArgs.add("-Xjsr305=strict") } }go deeper
Understands that an override must say whether parameters/returns are nullable.
Picks the override's nullability to match Java behavior and knows non-null params still get a runtime check.
Knows -Xjsr305=strict and JSpecify @NullMarked, and that the override signature is a binding contract for Kotlin callers while Java callers bypass it.
Sets the org-wide policy: annotate Java boundaries, enable strict flags, and reason about migration risk when tightening previously-platform signatures.
## Overriding unannotated Java methods Consider a Java interface: ```java interface Handler { String handle(String input); } // both unannotated ``` In Kotlin, `input` and the return are platform types. When you implement it, Kotlin makes you **commit** to an explicit nullability in the override signature — you can't leave them platform: ```kotlin class MyHandler : Handler { override fun handle(input: String): String = input.uppercase() // or: override fun handle(input: String?): String? = input?.uppercase() } ``` Both compile. The signature you choose becomes a **hard contract** for Kotlin callers: - Declaring `input: String` (non-null): Kotlin callers can't pass null — but the **Java caller** can still pass null, and because the parameter is non-null, an intrinsic check throws an NPE on entry. Choosing non-null asserts "Java never passes null here." - Declaring return `String?`: every Kotlin caller is now forced to null-handle, even if Java never returns null — possibly over-conservative. So the override is a deliberate decision: align it with the **real** Java behavior. ## Making the compiler stricter Unannotated values can't be made strict, but you can tighten how **annotated** Java is treated and surface issues: - `-Xjsr305=strict` — treat JSR-305 (`javax.annotation`) `@Nullable`/`@Nonnull` as real Kotlin nullability and **error** on violations instead of producing platform types. (`warn` and `ignore` are the looser modes.) - `-Xtype-enhancement-improvements-strict-mode` — stricter type-enhancement from annotation info. - **JSpecify** support: modern Kotlin honors `@NullMarked` packages so unannotated-but-marked code becomes non-null by default, eliminating platform types in that scope. The most reliable lever is **annotating the Java source**: per-element `@Nullable`/`@NotNull`, or package-level defaults (`@NonNullApi`/`@NullMarked`). Once annotated, the platform `T!` collapses to a precise `T` or `T?` and the compiler enforces it everywhere — overrides included. ## Practical checklist - Override signature = the **true** Java contract, not the convenient one. - Remember Java callers ignore your Kotlin non-null promise; non-null params fail fast via intrinsic checks. - Push `@Nullable/@NotNull` / `@NullMarked` onto the Java side to remove platform types at the root. - Use `-Xjsr305=strict` to escalate annotated-violation warnings to errors. ## Summary Platform types in inherited signatures force an explicit nullability decision in the override; that decision is a binding contract for Kotlin callers. Strictness comes from annotating Java and from compiler flags like `-Xjsr305=strict`, not from the unannotated values themselves.
- If your override declares a non-null parameter, can a null still reach it?Yes — Java callers don't see Kotlin's non-null guarantee, so they can pass null. Because the parameter is non-null, Kotlin's intrinsic parameter check throws an NPE on method entry, failing fast.
- What's the most durable way to stop platform types appearing at all?Annotate the Java side: per-member `@Nullable`/`@NotNull`, or package/module defaults like `@NonNullApi` / JSpecify `@NullMarked`. Then Kotlin infers precise `T`/`T?` instead of `T!`.
saying these in an interview costs you the question
- Thinking a Kotlin non-null override parameter protects against Java callers passing null
- Believing -Xjsr305=strict makes unannotated values strict (it only affects annotated ones)
- Not knowing overrides must commit to a nullability
- Ignoring package-level annotation defaults / JSpecify @NullMarked