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>
andBox<Animal>
are unrelated - Covariant:
Box<Cat>
is a subtype ofBox<Animal>
- Contravariant:
Box<Animal>
is a subtype ofBox<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:
// 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:
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:
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:
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:
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:
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:
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:
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
-
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. -
Immutability helps: Covariance works best with immutable data structures, which is why Kotlin's
List
is covariant butMutableList
is not. -
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:
- Use
out
to mark a type parameter as covariant - Covariant type parameters can only be used in "out" positions (return types)
- Covariance follows the "producer" pattern
- Kotlin's read-only collections are covariant, but mutable collections are invariant
Exercises
- Create a covariant
Box<T>
class that can only produce values but not consume them. - Implement a covariant
EventStream<T>
interface that has methods to subscribe to events of type T. - Create a class hierarchy with Animals and their specialized subclasses, then implement a covariant
Shelter<T>
that can adopt out animals. - Identify why
MutableList<T>
in Kotlin is not covariant butList<T>
is.
Further Reading
- Kotlin Official Documentation on Generics
- Declaration-site variance vs Use-site variance
- Type Projections in Kotlin
- Effective Java by Joshua Bloch - Chapter on Generics contains excellent information on variance
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)