Sling Academy
Home/Kotlin/Covariance and Contravariance Simplified in Kotlin

Covariance and Contravariance Simplified in Kotlin

Last updated: November 30, 2024

Covariance and contravariance are often tricky concepts when encountered in generic programming. However, once you grasp their essence, they can be very useful in designing robust and flexible APIs. Here, we'll break down these concepts using Kotlin as an example.

Covariance in Kotlin

Covariance allows you to specify that a type parameter can be substitutable with its subtypes. In Kotlin, you declare a type parameter to be covariant by using the out keyword. This is useful when you're designing classes and interfaces that only produce or return a type.

Consider the following example of a Producer interface where T is covariant:

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

class StringProducer : Producer<String> {
    override fun produce(): String {
        return "Hello World"
    }
}

fun printProducer(producer: Producer<Any>) {
    println(producer.produce())
}

fun main() {
    val stringProducer: Producer<String> = StringProducer()
    printProducer(stringProducer) // This is valid because Producer<String> is a subtype of Producer<Any>
}

In this example, Producer<String> can be safely passed to a function expecting Producer<Any> because String is a subtype of Any and the type parameter T in Producer< is covariant (it has the out modifier).

Contravariance in Kotlin

Contravariance, on the other hand, allows a type to be substitutable with its supertypes. You use the in keyword to make a type parameter contravariant. This makes sense when consuming or taking in objects of that type.

Take a look at this example involving a Consumer interface:

interface Consumer<in T> {
    fun consume(item: T)
}

class AnyConsumer : Consumer<Any> {
    override fun consume(item: Any) {
        println(item)
    }
}

fun consumeString(consumer: Consumer<String>) {
    consumer.consume("Kotlin Generics")
}

fun main() {
    val anyConsumer: Consumer<Any> = AnyConsumer()
    consumeString(anyConsumer) // This is valid because Consumer<String> is expected and Consumer<Any> is supplied
}

In this second example, Consumer<Any> can be passed to a function that expects Consumer<String> because the type parameter T in Consumer< is contravariant (marked with the in modifier), allowing this form of substitution.

Key Takeaways

  • Covariance (out): Allows you to read data from a structure, enables use of a type and its subtypes.
  • Contravariance (in): Allows you to write or accept data into a structure, enables use of a type and its supertypes.
  • Choosing out or in resolves around whether you consume or produce instances of a type.

Next Article: How to Use Reified Type Parameters in Inline Functions in Kotlin

Previous Article: Kotlin - Understanding Variance in Generics: `in` and `out`

Series: Advanced Kotlin Features

Kotlin

You May Also Like

  • How to Use Modulo for Cyclic Arithmetic in Kotlin
  • Kotlin: Infinite Loop Detected in Code
  • Fixing Kotlin Error: Index Out of Bounds in List Access
  • Setting Up JDBC in a Kotlin Application
  • Creating a File Explorer App with Kotlin
  • How to Work with APIs in Kotlin
  • What is the `when` Expression in Kotlin?
  • Writing a Script to Rename Multiple Files Programmatically in Kotlin
  • Using Safe Calls (`?.`) to Avoid NullPointerExceptions in Kotlin
  • Chaining Safe Calls for Complex Operations in Kotlin
  • Using the Elvis Operator for Default Values in Kotlin
  • Combining Safe Calls and the Elvis Operator in Kotlin
  • When to Avoid the Null Assertion Operator (`!!`) in Kotlin
  • How to Check for Null Values with `if` Statements in Kotlin
  • Using `let` with Nullable Variables for Scoped Operations in Kotlin
  • Kotlin: How to Handle Nulls in Function Parameters
  • Returning Nullable Values from Functions in Kotlin
  • Safely Accessing Properties of Nullable Objects in Kotlin
  • How to Use `is` for Nullable Type Checking in Kotlin