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