Skip to content
Generics with Variance
step 1/7

Reading — step 1 of 7

Learn

~3 min readGenerics, Delegation, Extensions

Kotlin in Action devotes an entire chapter to generics and variance. The vocabulary — in, out, * — looks small but unlocks the most subtle parts of the type system. Without variance you can't safely express List<Cat> as a List<Animal>.

Basic generics (review)

fun <T> singleton(x: T): List<T> = listOf(x)

class Box<T>(val value: T)

T is a type parameter. Multiple bounded constraints with where:

fun <T> max(items: List<T>): T?
where T : Comparable<T> {
    return items.maxOrNull()
}

The variance problem

Suppose Cat : Animal. Is List<Cat> a List<Animal>?

open class Animal
class Cat : Animal()

fun feedAll(list: List<Animal>) { /* ... */ }

val cats: List<Cat> = listOf(Cat(), Cat())
feedAll(cats)        // does this work?

In Kotlin, YES — because List<T> is covariant in T (declared with out). In Java, NO — List<T> is invariant by default.

The issue: if MutableList<Cat> were a MutableList<Animal>, you could add(Dog()) to it — heap pollution. So mutation breaks subtyping.

out — covariance ("producer")

out T says T is only PRODUCED (returned), never CONSUMED (taken as input):

interface Source<out T> {
    fun get(): T              // produces T — OK
    // fun set(x: T)         // would CONSUME — not allowed with out
}

val intSource: Source<Int> = ...
val anyySource: Source<Any> = intSource     // OK — covariant

Kotlin's read-only List<out T> is declared this way — you can read but not write, so subtyping is safe.

in — contravariance ("consumer")

in T says T is only CONSUMED, never PRODUCED:

interface Sink<in T> {
    fun put(x: T)             // consumes T — OK
    // fun get(): T           // would PRODUCE — not allowed with in
}

val anySink: Sink<Any> = ...
val intSink: Sink<Int> = anySink             // OK — contravariant (reversed)

If you can dump any Animal into a sink, you can certainly dump a Cat. Subtyping is reversed.

The PECS rule (from Java, applies here)

Producer Extends, Consumer Super. In Kotlin: out for producers, in for consumers.

fun <T> copy(source: Source<out T>, sink: Sink<in T>) {
    sink.put(source.get())
}

Source produces (out). Sink consumes (in). Now copy<Animal>(catSource, anySink) works because of the variance.

Use-site variance with in / out

If you can't change a class's declaration, you can specify variance at the USE site:

fun copyAll(source: List<out Animal>, dest: MutableList<in Animal>) {
    for (x in source) dest.add(x)
}

Like Java's ? extends and ? super. Less common in Kotlin because most stdlib types already declare variance correctly.

Star projection: <*>

"I don't know or care about T":

fun printAll(list: List<*>) {
    for (x in list) println(x)
}

Like Java's List<?>. Reads-only as Any?. Useful for utility code that doesn't depend on the element type.

reified — runtime access to T

Normally generics are erased — T::class doesn't compile. With inline + reified, T survives:

inline fun <reified T> Any.castOrNull(): T? = this as? T

val x: Any = "hi"
val s: String? = x.castOrNull<String>()      // works

Common mistakes

  • Using out where you write to T — compile error. out only allows reading.
  • Misreading variance — in/out describe the role of T in the API, not how you use the value.
  • Forgetting <out T> on read-only collection types — Kotlin's stdlib does this for you; rolling your own, remember to declare variance.
  • Star projection where you need a real TList<*> reads as Any?. If you need typed reads, use a generic function with bound T.
  • inline everything — bloat. Inline only when reified is needed or the function is small + hot.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…