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> and List<Int> are the same runtime class either way. Kotlin’s reified does 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’s List<out T> states it once, in the library’s own declaration, so ordinary calling code never needs a wildcard-equivalent at all.

See Also