Kotlin Variance

Variance describes how a generic type relates to its subtypes. If Dog is a subtype of Animal, what is the relationship between Box<Dog> and Box<Animal>? Variance answers that question using three rules: invariant, covariant, and contravariant.

Invariance (Default)

By default, generic types are invariant — Box<Dog> is completely unrelated to Box<Animal> even though Dog is a subtype of Animal>.

class Box(val item: T)

open class Animal(val name: String)
class Dog(name: String) : Animal(name)

fun processAnimalBox(box: Box) {
    println(box.item.name)
}

fun main() {
    val dogBox = Box(Dog("Rex"))
    // processAnimalBox(dogBox)   ← ERROR: Box is not Box
}

Covariance: out

Mark a type parameter with out to allow reading. A Producer<Dog> can then be used where a Producer<Animal> is expected because you only produce (output) values, never consume them.

class Producer(private val item: T) {
    fun produce(): T = item   // only returns T, never takes T as input
}

fun showAnimal(producer: Producer) {
    println(producer.produce().name)
}

fun main() {
    val dogProducer = Producer(Dog("Rex"))
    showAnimal(dogProducer)   // OK! Producer accepted as Producer
}

Contravariance: in

Mark a type parameter with in to allow writing. A Consumer<Animal> can be used where a Consumer<Dog> is expected because anything that consumes an Animal can also consume a Dog.

class Consumer {
    fun consume(item: T) {   // only takes T as input, never returns T
        println("Consuming: $item")
    }
}

fun feedDog(consumer: Consumer) {
    consumer.consume(Dog("Buddy"))
}

fun main() {
    val animalConsumer = Consumer()
    feedDog(animalConsumer)   // OK! Consumer accepted as Consumer
}

Variance Summary Diagram


                 Dog  is subtype of  Animal

Invariant  Box:
  Box  ←→  Box     UNRELATED (no direction)

Covariant  Producer:
  Producer  is subtype of  Producer
  (same direction as Dog → Animal)

Contravariant  Consumer:
  Consumer  is subtype of  Consumer
  (OPPOSITE direction to Dog → Animal)

Use-Site Variance (Projection)

You can also specify variance at the call site, not just the class definition:

fun copyAnimals(source: List, dest: MutableList) {
    for (item in source) {
        dest.add(item)
    }
}

fun main() {
    val dogs: List = listOf(Dog("Rex"), Dog("Buddy"))
    val animals: MutableList = mutableListOf()

    copyAnimals(dogs, animals)   // works due to projections
    animals.forEach { println(it.name) }
}

Star Projection

When you don't care about the specific type parameter, use *:

fun printSize(list: List<*>) {
    println("List has ${list.size} items")
}

fun main() {
    printSize(listOf(1, 2, 3))         // 3
    printSize(listOf("a", "b"))        // 2
    printSize(emptyList())    // 0
}

Practical Example: Data Pipeline

interface Source {
    fun next(): T
}

interface Sink {
    fun accept(item: T)
}

class NumberSource : Source {
    private var n = 0
    override fun next() = ++n
}

class PrintSink : Sink {
    override fun accept(item: Any) = println("Received: $item")
}

fun transfer(source: Source, sink: Sink, count: Int) {
    repeat(count) { sink.accept(source.next()) }
}

fun main() {
    transfer(NumberSource(), PrintSink(), 5)
}

Output:

Received: 1
Received: 2
Received: 3
Received: 4
Received: 5

Leave a Comment

Your email address will not be published. Required fields are marked *