You expose a Kotlin API using Unit and Nothing in generic positions to Java consumers. What erasure and interop pitfalls arise, and how do you design around them?
answer
- Generics erase ⇒ Unit/Nothing become Object slots
- () -> Unit forces Unit.INSTANCE in Java; use fun interface instead
- Nothing in generics loses bottom-type semantics for Java
- Don't leak List<Nothing>; pick a concrete element type
- Need runtime type? pass Class<T>, reified won't reach Java
basics
~20 sIn generics, Unit and Nothing erase to Object, and Java loses their special meaning. Java sees raw Function interfaces or Object slots, must return Unit.INSTANCE, and gets no 'never returns' guarantee — so design SAM/void APIs for Java.
solid answer
~40 sGeneric type arguments erase on the JVM, so `Function0<Unit>`, `List<Nothing>`, or a `Result<Nothing>`-style sentinel all collapse to raw `Object` slots at runtime. Consequences for Java consumers: (1) a `() -> Unit` parameter forces `return Unit.INSTANCE;`; (2) `Nothing` as a type argument conveys no bottom-type or never-returns semantics — Java just sees `Object`/`null`; (3) reified-free generic code can't distinguish these at runtime. Design fixes: prefer `fun interface` (SAM, `void` method) over raw function types; provide `@JvmStatic`/overloads that don't surface `Unit`/`Nothing` generics; for 'never returns' helpers, keep them as plain `void` methods and document the contract; avoid leaking `List<Nothing>` (use an explicit element type). The goal is a Java-ergonomic surface that doesn't depend on Kotlin-only type semantics.
code
kotlin · 6 lines// Java-hostile:
fun onEach(items: List<String>, fn: (String) -> Unit) { items.forEach(fn) }
// Java-friendly SAM equivalent (void method):
fun interface Consumer<T> { fun accept(t: T) }
fun onEachSam(items: List<String>, fn: Consumer<String>) { items.forEach(fn::accept) }
// Java: onEachSam(list, s -> System.out.println(s)); // no Unit.INSTANCEgo deeper
Understands that Java needs Unit.INSTANCE and that generics 'lose' type info.
Explains erasure to Object and the SAM-vs-function-type ergonomics fix.
Designs a Java-friendly surface (SAM/void, concrete types, Class<T> tokens) and explains why Nothing's semantics vanish.
Weighs cross-language API contracts, binary compatibility, and the limits of reified/erasure when defining a long-lived public surface.
## Background: erasure The JVM implements generics by **erasure** — type arguments are removed at compile time and replaced by their bound (usually `Object`). So `Function0<Unit>` and `List<Nothing>` have **no runtime trace** of `Unit`/`Nothing`; they are `Function0`/`List` of `Object`. `Unit` and `Nothing` are Kotlin **front-end** concepts; only `Unit` has a runtime singleton, and `Nothing` has no runtime value at all. ## Pitfall 1 — `() -> Unit` requires `Unit.INSTANCE` A `Function0<Unit>` parameter forces Java lambdas to `return kotlin.Unit.INSTANCE;`. Ugly and surprising for Java callers. ```kotlin fun retryFn(action: () -> Unit) { /* ... */ } // Java: return Unit.INSTANCE; fun interface Action { fun run() } fun retrySam(action: Action) { /* ... */ } // Java: clean void lambda ``` ## Pitfall 2 — `Nothing` carries no semantics in generics `Nothing` as a type argument (or a `Nothing`-returning generic factory) loses **all** bottom-type meaning to Java: no unreachable-code analysis, no exhaustiveness, no 'never returns'. Java sees `Object`/`null`. Even Kotlin's own runtime can't recover it without `reified` type tokens. ## Pitfall 3 — leaking `List<Nothing>` / empty sentinels Kotlin inference may produce `List<Nothing>` from `emptyList()`. Exposing that across the boundary confuses Java (raw `List`) and can box other code into impossible element types. Annotate the public element type explicitly. ## Pitfall 4 — no runtime distinction Because of erasure, you **cannot** branch at runtime on whether a generic argument was `Unit`, `Nothing`, or anything else, unless you carry a `Class<T>`/reified token. `inline fun <reified T>` only helps inside Kotlin, not for the Java-facing signature. ## Design guidance - **Prefer SAM `fun interface`** with `Unit`-returning (⇒ `void`) methods over raw function types for Java-facing callbacks. - **Don't surface `Nothing`** in public generic signatures; pick a concrete type or expose a non-generic overload. - For 'never returns' utilities, keep a **plain `void` method** and document the contract; Java won't get compile-time benefit but the API stays clean. - Use `@JvmName`, `@JvmStatic`, and explicit overloads to keep the Java view free of `Unit.INSTANCE` boilerplate. - If runtime type info is needed, pass a `Class<T>` token rather than relying on the erased argument. ## Summary Erasure flattens `Unit`/`Nothing` generics to `Object`; `Unit` forces `Unit.INSTANCE`, `Nothing` loses its semantics, and nothing is recoverable at runtime without tokens. Design Java-facing APIs with SAM/`void` methods and concrete types.
- Does `inline fun <reified T>` help a Java caller distinguish Unit vs Nothing at runtime?No. `reified` only retains the type inside Kotlin inline call sites; the Java-facing signature is still erased. For Java you must pass a `Class<T>` token.
- How do you keep a 'never returns' helper usable from Java?Expose it as a normal `void` method (Nothing return lowers to void) and document the contract. Java gets no unreachable-code analysis but the call works fine.
saying these in an interview costs you the question
- Believing Java can recover Unit/Nothing from an erased generic at runtime
- Exposing raw () -> Unit function types as the primary Java API
- Thinking reified bridges the type to Java callers
- Leaking List<Nothing> as a public element type