skip to content

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?

level: seniorimportance: nice to knowfreq 22%

answer

  1. Generics erase ⇒ Unit/Nothing become Object slots
  2. () -> Unit forces Unit.INSTANCE in Java; use fun interface instead
  3. Nothing in generics loses bottom-type semantics for Java
  4. Don't leak List<Nothing>; pick a concrete element type
  5. Need runtime type? pass Class<T>, reified won't reach Java

basics

~20 s

In 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 s

Generic 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
kotlin
// 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.INSTANCE

go deeper

for a junior

Understands that Java needs Unit.INSTANCE and that generics 'lose' type info.

for a middle

Explains erasure to Object and the SAM-vs-function-type ergonomics fix.

for a senior

Designs a Java-friendly surface (SAM/void, concrete types, Class<T> tokens) and explains why Nothing's semantics vanish.

for a principal

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

context