Skip to main content

Kotlin Covariance

Introduction

When working with generic types in Kotlin, you'll eventually encounter situations where you need to understand how subtypes and supertypes relate to each other in the context of generics. Covariance is one of these important type relationships that enables more flexible and intuitive code when working with generics.

In simple terms, covariance allows you to use a more derived (more specific) type than originally specified. It's indicated by the out keyword in Kotlin and enables a generic type with a more specific type argument to be used where a generic type with a less specific type argument is expected.

Understanding Type Variance

Before diving into covariance, let's understand what type variance means:

Type variance addresses the question: if Cat is a subtype of Animal, what's the relationship between Box<Cat> and Box<Animal>?

There are three possible answers:

  • Invariant: Box<Cat> and Box<Animal> are unrelated
  • Covariant: Box<Cat> is a subtype of Box<Animal>
  • Contravariant: Box<Animal> is a subtype of Box<Cat>

In this lesson, we'll focus on covariance.

Covariance with the out Keyword

In Kotlin, we make a generic type covariant by using the out keyword:

kotlin
// Covariant type parameter T
interface Producer<out T> {
fun produce(): T
}

This means that a Producer<Cat> is a subtype of Producer<Animal>, allowing you to use the more specific type where the more general one is expected.

Let's see an example:

kotlin
open class Animal {
fun feed() = println("Animal is eating")
}

class Cat : Animal() {
fun meow() = println("Meow!")
}

// A covariant producer
interface AnimalProducer<out T : Animal> {
fun produce(): T
}

// Specific implementation for cats
class CatProducer : AnimalProducer<Cat> {
override fun produce(): Cat {
return Cat()
}
}

fun feedAnimals(producer: AnimalProducer<Animal>) {
val animal = producer.produce()
animal.feed()
}

fun main() {
val catProducer = CatProducer()
// This works because CatProducer is a subtype of AnimalProducer<Animal>
// thanks to covariance
feedAnimals(catProducer)
}

Output:

Animal is eating

Notice how we passed a CatProducer (which is an AnimalProducer<Cat>) to a function that expects an AnimalProducer<Animal>. This wouldn't be possible without covariance.

When to Use Covariance: The "Producer" Rule

A simple rule to remember when to use covariance is the "producer" rule:

Use out (covariance) when your class only produces or returns the generic type, but never consumes it as input parameters.

This makes sense because it's safe to produce more specific types than expected. The out keyword actually enforces this safety:

kotlin
interface Producer<out T> {
fun produce(): T // OK - returning (producing) T

// This would not compile:
// fun consume(item: T) // Error: T is a covariant type parameter, but occurs in IN position
}

Practical Example: Collections

Kotlin's collections API makes extensive use of covariance. For example, List<T> in Kotlin is defined as interface List<out T>, which means it's covariant:

kotlin
fun printAnimalNames(animals: List<Animal>) {
animals.forEach { println("Animal name: ${it.javaClass.simpleName}") }
}

fun main() {
val cats: List<Cat> = listOf(Cat(), Cat())
printAnimalNames(cats) // This works because List<Cat> is a subtype of List<Animal>
}

Output:

Animal name: Cat
Animal name: Cat

This works because List<T> in Kotlin is designed as a "read-only" collection - you can get items out of it, but not put items into it. This aligns perfectly with our "producer" rule.

Covariance on Type Projections

Sometimes, you don't want to declare an entire class as covariant, but only want to use covariance at a specific point. Kotlin allows this with type projections:

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

fun copyFromCatToAnimal(from: Box<Cat>, to: Box<Animal>) {
to.value = from.value // Copying a Cat to an Animal reference is safe
}

fun main() {
val catBox = Box(Cat())
val animalBox = Box<Animal>(Animal())

copyFromCatToAnimal(catBox, animalBox)
animalBox.value.feed() // Animal is eating
}

But what if we wanted to make this function more generic? We can use the out projection:

kotlin
fun <T> copyOut(from: Box<out T>, to: Box<T>) {
// from is projected to be covariant - you can only read from it
to.value = from.value
}

fun main() {
val catBox = Box(Cat())
val animalBox = Box<Animal>(Animal())

// This works because catBox is treated as Box<out Animal>
copyOut(catBox, animalBox)
}

The out projection in Box<out T> means you can only read values of type T from the from box, not write to it.

Real-World Use Cases for Covariance

Covariance in Stream Processing

When processing streams of data, you often want to transform one type into another:

kotlin
interface DataProcessor<out T> {
fun process(): T
}

class JsonProcessor : DataProcessor<Map<String, Any>> {
override fun process(): Map<String, Any> {
// Process JSON and return a map
return mapOf("name" to "Kotlin", "version" to 1.5)
}
}

class XmlProcessor : DataProcessor<Map<String, String>> {
override fun process(): Map<String, String> {
// Process XML and return a map
return mapOf("language" to "Kotlin")
}
}

// This function accepts any DataProcessor that produces at least a Map<String, Any>
fun processData(processor: DataProcessor<Map<String, Any>>) {
val result = processor.process()
println("Processed data: $result")
}

fun main() {
val jsonProcessor = JsonProcessor()
processData(jsonProcessor) // Works fine

// This won't work because Map<String, String> is not a subtype of Map<String, Any>
// processData(XmlProcessor())
}

UI Component Hierarchy

Consider a UI component system:

kotlin
interface Component
class Button : Component
class ToggleButton : Button

interface ComponentFactory<out T : Component> {
fun create(): T
}

class ButtonFactory : ComponentFactory<Button> {
override fun create() = Button()
}

class ToggleButtonFactory : ComponentFactory<ToggleButton> {
override fun create() = ToggleButton()
}

fun createUI(factory: ComponentFactory<Button>) {
val button = factory.create()
// Do something with the button
println("Created button: ${button.javaClass.simpleName}")
}

fun main() {
val buttonFactory = ButtonFactory()
val toggleButtonFactory = ToggleButtonFactory()

createUI(buttonFactory)
// This works because ToggleButtonFactory is a subtype of ComponentFactory<Button>
// thanks to covariance
createUI(toggleButtonFactory)
}

Output:

Created button: Button
Created button: ToggleButton

Limitations and Considerations

  1. Covariant types can only appear in "out" positions: When you declare a type parameter as covariant with out, you can only use it as a return type, not as a parameter type.

  2. Immutability helps: Covariance works best with immutable data structures, which is why Kotlin's List is covariant but MutableList is not.

  3. Array covariance issues: Unlike Java, Kotlin's arrays (Array<T>) are invariant to prevent potential runtime issues.

Summary

Covariance in Kotlin, marked by the out keyword, allows you to use a more specific type where a more general type is expected in generic contexts. This is particularly useful for types that mainly "produce" or "return" values, like read-only collections or factories.

Remember the key points:

  1. Use out to mark a type parameter as covariant
  2. Covariant type parameters can only be used in "out" positions (return types)
  3. Covariance follows the "producer" pattern
  4. Kotlin's read-only collections are covariant, but mutable collections are invariant

Exercises

  1. Create a covariant Box<T> class that can only produce values but not consume them.
  2. Implement a covariant EventStream<T> interface that has methods to subscribe to events of type T.
  3. Create a class hierarchy with Animals and their specialized subclasses, then implement a covariant Shelter<T> that can adopt out animals.
  4. Identify why MutableList<T> in Kotlin is not covariant but List<T> is.

Further Reading



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