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.
Table of Contents
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
outorinresolves around whether you consume or produce instances of a type.