Skip to main content

Kotlin Variance

When working with generic types in Kotlin, you'll eventually encounter a situation where you need to understand how subtypes and supertypes behave in generic contexts. This concept, called variance, is crucial for writing flexible and type-safe code.

What is Variance?

Variance determines how subtyping between more complex types relates to subtyping between their components. In simpler terms, if Cat is a subtype of Animal, what's the relationship between Box<Cat> and Box<Animal>?

Kotlin provides three types of variance:

  • Invariance (default)
  • Covariance (using out keyword)
  • Contravariance (using in keyword)

Let's explore each one in detail.

Invariance (Default Behavior)

By default, generic types in Kotlin are invariant. This means that Box<Cat> and Box<Animal> have no subtyping relationship, even though Cat is a subtype of Animal.

kotlin
// Invariant class
class Box<T>(val item: T) {
fun getItem(): T = item
fun setItem(newItem: T) {
// Imagine we could modify the item
}
}

fun main() {
val catBox = Box<Cat>(Cat("Whiskers"))

// This won't compile - type mismatch
// val animalBox: Box<Animal> = catBox
}

Invariance is the safest default since it prevents potential type errors.

Covariance (out)

Covariance allows you to use a more derived type (subtype) in place of a less derived type (supertype). In Kotlin, we use the out keyword to make a type parameter covariant.

kotlin
// Covariant class
class ReadOnlyBox<out T>(val item: T) {
fun getItem(): T = item
// Notice we can't have functions that take T as input
}

open class Animal(val name: String) {
fun breathe() = println("$name is breathing")
}

class Cat(name: String) : Animal(name) {
fun meow() = println("$name says: Meow")
}

fun main() {
val catBox = ReadOnlyBox(Cat("Whiskers"))

// This works! Box<Cat> is a subtype of Box<Animal> due to covariance
val animalBox: ReadOnlyBox<Animal> = catBox

// We can still access the item as an Animal
animalBox.getItem().breathe() // Output: Whiskers is breathing

// But we can't access Cat-specific methods
// animalBox.getItem().meow() // This won't compile
}

Notice that with a covariant type parameter, you can only use that type in "out" positions—that is, as return types, not as parameter types.

Contravariance (in)

Contravariance is the opposite of covariance. It allows you to use a less derived type (supertype) in place of a more derived type (subtype). We use the in keyword in Kotlin to indicate contravariance.

kotlin
// Contravariant class
class AnimalProcessor<in T> {
fun process(item: T) {
println("Processing ${item.toString()}")
}

// Notice we can't have functions that return T
}

fun main() {
val animalProcessor = AnimalProcessor<Animal>()

// This works! AnimalProcessor<Animal> is a subtype of AnimalProcessor<Cat>
val catProcessor: AnimalProcessor<Cat> = animalProcessor

catProcessor.process(Cat("Whiskers")) // Output: Processing Cat@...
}

With contravariance, you can only use the type in "in" positions, such as parameter types, not as return types.

Understanding Type Projection

Sometimes you may need variance for a specific usage of a type rather than for the type itself. Kotlin allows this through type projections.

Use-site Variance (Type Projections)

kotlin
class Box<T>(var value: T)

fun copyFromTo(from: Box<out Any>, to: Box<Any>) {
to.value = from.value // This works because 'from' is projected as covariant
}

fun main() {
val stringBox = Box("Hello")
val anyBox = Box<Any>(42)

copyFromTo(stringBox, anyBox)
println(anyBox.value) // Output: Hello
}

In the example above, Box<out Any> is a projected type, meaning it can be a Box<String>, Box<Int>, or any other Box<T> where T is a subtype of Any.

Star Projection

When you don't care about the specific type arguments, you can use star projections:

kotlin
fun printBoxContent(box: Box<*>) {
// We can safely read the value as Any?
println("Box contains: ${box.value}")

// But we can't write to it
// box.value = "Something" // This won't compile
}

fun main() {
val stringBox = Box("Hello")
val intBox = Box(42)

printBoxContent(stringBox) // Output: Box contains: Hello
printBoxContent(intBox) // Output: Box contains: 42
}

The star projection is similar to Box<out Any?> but more concise.

Real-world Applications of Variance

Example 1: Collection APIs

Kotlin's collections make excellent use of variance:

kotlin
// List is covariant in its type parameter
fun printAnimals(animals: List<Animal>) {
for (animal in animals) {
println("Animal: ${animal.name}")
}
}

fun main() {
val cats = listOf(Cat("Whiskers"), Cat("Mittens"))

// This works because List<Cat> is a subtype of List<Animal> (covariance)
printAnimals(cats) // Will print both cats

// MutableList, however, is invariant
val mutableCats: MutableList<Cat> = mutableListOf(Cat("Felix"))
// The following would not compile:
// val mutableAnimals: MutableList<Animal> = mutableCats
}

Example 2: Event Handlers

Contravariance is useful for callbacks and event handlers:

kotlin
interface EventListener<in T> {
fun onEvent(event: T)
}

class ButtonClickEvent
class MouseEvent : ButtonClickEvent()

fun main() {
// A listener that can handle any ButtonClickEvent
val buttonListener = object : EventListener<ButtonClickEvent> {
override fun onEvent(event: ButtonClickEvent) {
println("Button was clicked")
}
}

// Because EventListener is contravariant, we can use a ButtonClickEvent listener
// where a MouseEvent listener is expected
val mouseListener: EventListener<MouseEvent> = buttonListener

mouseListener.onEvent(MouseEvent()) // Output: Button was clicked
}

Example 3: Generic Factory

Covariance is useful for factories or producers:

kotlin
interface Producer<out T> {
fun produce(): T
}

class AnimalProducer : Producer<Animal> {
override fun produce(): Animal = Animal("Generic Animal")
}

class CatProducer : Producer<Cat> {
override fun produce(): Cat = Cat("New Cat")
}

fun feedAnimal(producer: Producer<Animal>) {
val animal = producer.produce()
println("Feeding ${animal.name}")
}

fun main() {
val animalProducer = AnimalProducer()
val catProducer = CatProducer()

feedAnimal(animalProducer) // Output: Feeding Generic Animal

// This works because Producer is covariant
feedAnimal(catProducer) // Output: Feeding New Cat
}

Guidelines for Using Variance

  1. Use out (covariance) when your class only returns (produces) values of type T, and never consumes them as parameters.

  2. Use in (contravariance) when your class only consumes values of type T as parameters, and never produces them as return values.

  3. Default to invariance if your class both consumes and produces values of type T.

  4. Consider use-site variance with type projections for more flexible API design.

Summary

Variance in Kotlin allows you to create more flexible yet type-safe generic code:

  • Invariance (default): No relationship between Box<Cat> and Box<Animal>
  • Covariance (out): Box<Cat> is a subtype of Box<Animal> if Cat is a subtype of Animal
  • Contravariance (in): Box<Animal> is a subtype of Box<Cat> if Cat is a subtype of Animal

Understanding variance is crucial when designing generic classes and APIs, as it affects how your types can be used in different contexts.

Exercises

  1. Create a generic class Stack<T> and determine whether it should be invariant, covariant, or contravariant.

  2. Implement a Transformer<in I, out O> class that transforms inputs of type I to outputs of type O.

  3. Create a function that can copy elements from any list to a List<Any>, using appropriate variance annotations.

Additional Resources

Understanding variance will help you write more reusable, type-safe code that properly expresses your design intentions.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)