Generics and Variance
|
This section documents Kotlin 2.4.x on the JVM, as published at kotlinlang.org, which is the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against kotlinlang.org before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Kotlin generics share Java’s erasure model at the bytecode level, but let variance be declared once, on the type itself, instead of repeated at every use site the way Java’s wildcards require.
Generic Classes and Functions
class Box<T>(var value: T)
val intBox = Box(5) // T inferred as Int
val stringBox: Box<String> = Box("hi")
fun <T> firstOrNull(list: List<T>): T? = if (list.isEmpty()) null else list[0]
Declaration-Site Variance: in and out
Kotlin lets a generic type parameter be marked out (covariant — the type may only be produced, never
consumed) or in (contravariant — only consumed, never produced) once, on the class declaration itself.
Every use of that type then inherits the variance automatically — no wildcard is repeated at each call site the
way Java’s ? extends/? super would require:
interface Producer<out T> { // "out": T only ever appears as a return type
fun produce(): T
}
interface Consumer<in T> { // "in": T only ever appears as a parameter type
fun consume(item: T)
}
fun printAll(producer: Producer<Any>) { /* ... */ }
val stringProducer: Producer<String> = object : Producer<String> {
override fun produce() = "hello"
}
printAll(stringProducer) // legal: Producer<String> is a subtype of Producer<Any> because T is "out"
This mirrors the PECS mnemonic from Java generics ("Producer Extends, Consumer Super") but states the rule
once at the type’s definition rather than at every call site that uses ? extends T/? super T. Kotlin’s
read-only List<out T> and mutable MutableList<T> (invariant, since it both produces via get and consumes
via add) in the standard library are themselves declared exactly this way.
Use-Site (Star) Projections
When only part of a generic type’s usage needs variance, or the exact type argument is unknown and
irrelevant, Kotlin allows a use-site star projection (*) — roughly Kotlin’s equivalent of Java’s unbounded
wildcard ?:
fun printBoxContents(box: Box<*>) { // don't care what T is -- just read it as Any?
println(box.value)
}
Box<*> is treated as if it were Box<out Any?> for reading purposes — you can read value (typed Any?)
but cannot call anything that would require writing a specific T into it.
Reified Type Parameters with inline Functions
Java erases generic type information at compile time — T is not available at runtime inside a generic
method, so list is List<String> cannot be checked and T::class.java cannot be called. Kotlin lifts this
restriction, but only for an inline function’s type parameter, marked reified: because the function body
is copied into every call site (see
Lambdas and Higher-Order Functions), the
compiler can substitute the real type argument directly into the inlined code:
inline fun <reified T> isInstance(value: Any): Boolean = value is T
println(isInstance<String>("hello")) // true -- T is available at the call site, unlike plain generics
inline fun <reified T> Gson.fromJson(json: String): T =
this.fromJson(json, T::class.java) // T::class.java would not compile without "reified"
reified is only legal on an inline function’s type parameter — it depends entirely on the function being
inlined, since a non-inlined generic method still has no way to know its type argument at runtime.
Contrast with Java
-
Type erasure happens identically in both languages at the JVM bytecode level —
List<String>andList<Int>are the same runtime class either way. Kotlin’sreifieddoes not defeat erasure at the bytecode level; it works around it at the source level by copying the type-specialized code into each call site. -
Wildcards vs. declaration-site variance — Java’s
List<? extends Number>repeats the variance annotation at every use; Kotlin’sList<out T>states it once, in the library’s own declaration, so ordinary calling code never needs a wildcard-equivalent at all.
See Also
-
Collections and Sequences —
List<out T>vs.MutableList<T>in practice. -
Lambdas and Higher-Order Functions —
inline, the prerequisite forreified. -
Type-Safe Builders and DSLs — another common use of
inline+reifiedtogether.