thecodex.expert · The Codex Family of Knowledge
Kotlin

Generics

Java repeats ? extends Animal at every single call site that needs covariance — Kotlin lets the class declare out Animal ONCE, and every user of that class benefits automatically.

Kotlin 2.1 Declaration-site variance Last verified:
Canonical Definition

A generic class or function declares type parameters in angle brackets, same as Java's syntax on the surface. The real difference is variance: Kotlin supports DECLARATION-site variance, where the class itself marks a type parameter out (the type is only ever produced/returned, enabling safe covariance — a Producer<Cat> can be used where Producer<Animal> is expected) or in (only ever consumed/accepted, enabling contravariance). Java instead requires USE-site variance, repeating ? extends T or ? super T at every call site that needs it — Kotlin's approach means the variance is decided once, by the class author, and every caller benefits automatically without repeating any wildcard syntax.

A basic generic class

Box<T> works identically to a generic class in Java or C++ at this basic level — the interesting differences show up specifically around variance.

Kotlinbasic_generic.kt
class Box<T>(private val item: T) {
    fun get(): T = item
}

val intBox = Box(42)
val stringBox = Box("hello")

out: declaration-site covariance

Marking T as out ONCE, in Producer's own declaration, means every user of Producer automatically gets safe covariance — a Producer<Cat> can be passed anywhere a Producer<Animal> is expected, with no wildcard syntax needed at any call site.

Kotlinout_variance.kt
open class Animal
class Cat : Animal()

interface Producer<out T> {   // out: T is only ever PRODUCED/returned, never consumed
    fun produce(): T
}

fun printAnimal(producer: Producer<Animal>) {
    println(producer.produce())
}

val catProducer: Producer<Cat> = object : Producer<Cat> {
    override fun produce() = Cat()
}

printAnimal(catProducer)   // fine — Producer<Cat> accepted where Producer<Animal> is expected

in: declaration-site contravariance

in is the mirror image — T is only ever consumed (accepted as a parameter), never produced, which safely allows the reverse substitution: a Consumer<Animal> can be used anywhere a Consumer<Cat> is expected.

Kotlinin_variance.kt
interface Consumer<in T> {   // in: T is only ever CONSUMED/accepted, never produced
    fun consume(item: T)
}

val animalConsumer: Consumer<Animal> = object : Consumer<Animal> {
    override fun consume(item: Animal) { println("Consumed an animal") }
}

val catConsumer: Consumer<Cat> = animalConsumer   // fine — Consumer<Animal> works as Consumer<Cat>

Sources

1
JetBrains. Kotlin Documentation, "Generics: in, out, where," kotlinlang.org/docs/generics.html.