0
0
KotlinComparisonIntermediate · 4 min read

Covariance vs Contravariance in Kotlin: Key Differences and Usage

In Kotlin, 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.

AspectCovariance (out)Contravariance (in)
Variance Keywordoutin
Type RelationshipPreserves subtype relation (Producer)Reverses subtype relation (Consumer)
UsageReturn types, output onlyInput parameters, consume only
ExampleList can hold ListComparator can accept Comparator
SafetySafe to read, unsafe to writeSafe 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.

kotlin
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
}
Output
Produced number: 10
↔️

Contravariance Equivalent

This example shows contravariance with the in keyword, allowing a function to accept a consumer of Int or its supertypes.

kotlin
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
}
Output
Consumed number: 5
🎯

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

Use out for covariance when your type produces values safely.
Use in for contravariance when your type consumes values safely.
Covariance preserves subtype relationships; contravariance reverses them.
Kotlin enforces variance at compile time to prevent type errors.
Choosing the right variance improves code flexibility and safety.