Covariance vs Contravariance in Kotlin: Key Differences and Usage
covariance allows a generic type to preserve subtype relationships (using out), meaning you can use a subtype where a supertype is expected. Contravariance (using in) reverses this, allowing a supertype to be used where a subtype is expected, mainly for input parameters.Quick Comparison
Here is a quick side-by-side comparison of covariance and contravariance in Kotlin.
| Aspect | Covariance (out) | Contravariance (in) |
|---|---|---|
| Variance Keyword | out | in |
| Type Relationship | Preserves subtype relation (Producer) | Reverses subtype relation (Consumer) |
| Usage | Return types, output only | Input parameters, consume only |
| Example | List | Comparator |
| Safety | Safe to read, unsafe to write | Safe to write, unsafe to read |
Key Differences
Covariance in Kotlin is declared with the out keyword and means that a generic type can only produce or return values of the specified type or its subtypes. This allows you to use a more specific type where a more general type is expected, like using List<out Number> to accept lists of Int or Double. Covariant types are safe to read from but not safe to write to because writing could break type safety.
Contravariance uses the in keyword and means the generic type can only consume or accept values of the specified type or its supertypes. This reverses the subtype relationship, allowing you to use a more general type where a more specific type is expected. For example, Comparator<in Number> can compare Int values. Contravariant types are safe to write to but not safe to read from.
In summary, covariance is about producing values safely, while contravariance is about consuming values safely. Kotlin enforces these rules at compile time to prevent type errors.
Code Comparison
This example shows covariance with the out keyword, allowing a function to accept a producer of Number or its subtypes.
interface Producer<out T> {
fun produce(): T
}
class NumberProducer : Producer<Number> {
override fun produce(): Number = 42
}
fun useProducer(producer: Producer<Number>) {
val number: Number = producer.produce()
println("Produced number: $number")
}
fun main() {
val intProducer: Producer<Int> = object : Producer<Int> {
override fun produce(): Int = 10
}
useProducer(intProducer) // Allowed because of covariance
}Contravariance Equivalent
This example shows contravariance with the in keyword, allowing a function to accept a consumer of Int or its supertypes.
interface Consumer<in T> {
fun consume(item: T)
}
class NumberConsumer : Consumer<Number> {
override fun consume(item: Number) {
println("Consumed number: $item")
}
}
fun useConsumer(consumer: Consumer<Int>) {
consumer.consume(5)
}
fun main() {
val numberConsumer: Consumer<Number> = NumberConsumer()
useConsumer(numberConsumer) // Allowed because of contravariance
}When to Use Which
Choose covariance (out) when your generic type only produces or returns values, like reading from a collection. It ensures type safety when you want to accept subtypes.
Choose contravariance (in) when your generic type only consumes or accepts values, like passing values to a function or comparator. It allows you to accept supertypes safely.
Using the correct variance keyword helps Kotlin enforce type safety and avoid runtime errors.
Key Takeaways
out for covariance when your type produces values safely.in for contravariance when your type consumes values safely.