skip to content

When Kotlin calls a Java API that uses wildcards like `List<? extends Number>`, how does Kotlin see that type, and what pitfalls arise around platform types and variance?

level: seniorimportance: nice to knowfreq 25%

answer

  1. ? extends -> out, ? super -> in, ? -> *
  2. Java types -> platform types with `!`
  3. platform type = relaxed null checks -> possible NPE
  4. (Mutable)List flexibility can surprise
  5. annotate Java @Nullable/@NotNull + -Xjsr305=strict

basics

~20 s

Kotlin translates Java's ? extends Number into out Number and ? super Number into in Number. Java types without nullability info become platform types (Number!), so you must be careful about nulls and what you can read or write.

solid answer

~40 s

Crossing from Java to Kotlin, the compiler maps wildcards back to projections: `List<? extends Number>` becomes `(Mutable)List<out Number>!`, `List<? super Number>` becomes `(Mutable)List<in Number>!`, and `List<?>` becomes `(Mutable)List<*>!`. The trailing `!` marks a **platform type** — Java carried no nullability metadata, so the Kotlin compiler relaxes null-checks, and a careless dereference can throw `NullPointerException` at runtime. Variance is preserved: you still can't `add` to an `out`-projected list nor read a useful type from an `in`-projected one. Pitfalls: (1) platform types defeat null-safety, so annotate Java with `@Nullable`/`@NotNull` or handle defensively; (2) `MutableList` vs `List` flexibility (`(Mutable)List`) means Kotlin can treat the same Java `List` as read-only or mutable, which can surprise callers.

code

kotlin · 6 lines
kotlin
// Java: static <T> T head(java.util.List<? extends T> xs) { ... }

val nums: List<Int> = listOf(1, 2)
val h = JavaUtil.head(nums)   // h: Int!  (platform type)
// Safe handling:
val safe: Int = h ?: 0        // treat platform value as nullable

go deeper

for a junior

Knows Java types can be null and Kotlin maps wildcards to in/out.

for a middle

Explains the wildcard->projection table and basic platform-type nullability risk.

for a senior

Details platform-type NPE hazards, (Mutable)List flexibility, and -Xjsr305=strict mitigation.

for a principal

Designs an interop strategy (annotations, strict flags, wrapper layers) to remove platform-type risk across a large Java/Kotlin codebase.

## Wildcard -> projection translation (Java to Kotlin) When Kotlin consumes a Java signature, wildcards become use-site projections: | Java | Kotlin sees | |------|-------------| | `List<? extends Number>` | `(Mutable)List<out Number>!` | | `List<? super Number>` | `(Mutable)List<in Number>!` | | `List<?>` | `(Mutable)List<*>!` | | raw `List` | `(Mutable)List<*>!` | Variance semantics are preserved: an `out`-projected list rejects writes; an `in`-projected list yields only `Any?` on reads. ## Platform types — the `!` suffix A **platform type** (written `Number!`, `List<Number>!` — you can't write `!` yourself) is a type that came from Java with **no nullability information**. Kotlin relaxes its usual null checks for it: you may treat it as nullable or non-null, and the compiler won't force a `?` check. ```kotlin // Java: Number firstNumber(List<? extends Number> xs) val n = Java.firstNumber(list) // n: Number! — could be null at runtime! val x: Int = n.toInt() // no compile error, but NPE if n was null ``` The danger: code compiles cleanly but throws **`NullPointerException`** at the dereference if Java returned `null`. ## `(Mutable)List` flexibility Kotlin maps Java's `java.util.List` to a *flexible* type `(Mutable)List<...>!`, meaning it can be used where either the read-only `kotlin.collections.List` or `MutableList` is expected. This lets you call mutating methods, but if you assign to a read-only `List` and the underlying Java collection is immutable, a mutation attempt throws `UnsupportedOperationException`. ## Pitfalls and mitigations - **Null-safety hole**: add JSR-305 / `org.jetbrains.annotations` `@Nullable`/`@NotNull` (or `javax.annotation`) to the Java side so Kotlin infers real nullable/non-null types instead of platform types. The `-Xjsr305=strict` flag makes the compiler honor them strictly. - **Accidental mutation**: be explicit — declare the Kotlin variable as `List<...>` (read-only) when you don't intend to mutate. - **Over-reading from `in` projections**: remember an `in Number` list reads back only `Any?`. - **Star-projection from raw types**: a Java raw `List` arrives as `List<*>!`, so you can read `Any?` but not add. ## Summary Variance round-trips cleanly (wildcards <-> projections), but the *real* interop hazard is platform-type nullability, not variance. Annotate Java, prefer explicit read-only types, and treat platform values as nullable until proven otherwise.

  • Why doesn't Kotlin just make every Java return type nullable?
    It would force `!!`/`?.` everywhere, making Java interop painfully verbose; platform types are a pragmatic compromise that trusts the developer.
  • How can you eliminate platform types from a Java dependency?
    Add `@NotNull`/`@Nullable` (JetBrains or JSR-305) annotations on the Java side and compile Kotlin with `-Xjsr305=strict`.
  • What does `List<? super Number>` let you read in Kotlin?
    Only `Any?` — it is `in`-projected (contravariant), so the element type is unknown on the read side.

saying these in an interview costs you the question

  • Claiming Java types come into Kotlin as non-null by default
  • Saying platform types cause compile errors (they cause runtime NPEs)
  • Forgetting variance is preserved across the boundary
  • Believing you can write to an `out`-projected Java list
  • Ignoring the read-only vs MutableList flexibility hazard

context