闭门造车姚师傅

Covariance and Contravariance in Kotlin

字数统计: 557阅读时长: 3 min
2017/10/27 Share

What is the hell of invariance, covariance and contravariance in Kotlin , and why this happen. It’s also helpful for understanding wildcast in Java.

Invariance, Why in and out mechanism

When you call addAnswer() , the generic type must be Number , otherwise, you will get a Type mismatch: inferred type is MutableList<Int> but MutableList<Number> was expected alike compile time error.

1
2
3
4
fun addAnswer(list: MutableList<Number>) {
list.add(42)
}
addAnswer(mutableListOf<Number>())

There are two and only two conditions if you don’t provide the same type declared in function parameter.

  1. type is more specific. The MutableList\<Int> do not take a Double value.
  2. type is more general. There is no guarantee on what you get from the collection is a number or something else.

This is called invariance.


In the following part, we are using Producer-Consumer problem as sample to look inside contravariance and covariance.

There is no doubt that generic is powerful and flexible. The invariance of collection is a big trouble. And the implementation of generic in Java , type info be erased after compile makes it much harder to handle this.

out, Covariance

1
2
3
4
5
6
7
interface Producer<out T> {  // notice the `out` notation
fun produce(): T
}

fun <T> produce(producer: Producer<T>) {
println("Produced: ${producer.produce()}")
}
1
2
3
4
5
6
7
8
val producerAnimal = object : Producer<Animal> {
override fun produce(): Animal {
return Animal()
}
}
produce<Creature>(producerAnimal) // alright
produce<Animal>(producerAnimal) // alright
produce<Pig>(producerAnimal) // Not complile: Type Mismatch

By adding out notation at the generic declaration, now we can using instances whose generic type is a subtype of the expected generic type. As is when Producer<Number> is expected, you can pass Producer<Int> as well, but Producer<Any> is unacceptable.

The out notation can get rid of the first risk(type is more specific. The MutableList\<Int> do not take a Double value. ) introduced in the former part. Because we do not use these as args, so we are not gonna violet the first limitation.

in, Contravariance

1
2
3
4
5
6
7
interface Consumer<in T> {
fun consume(item: T)
}

fun <U> List<U>.consume(consumer: Consumer<U>) {
this.forEach(::println)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
val consumerCreature = object : Consumer<Creature> {
override fun consume(item: Creature) {
println("Consuming Creature")
}
}
val consumerAnimal = object : Consumer<Animal> {
override fun consume(item: Animal) {
println("Consuming Animal")
}
}
val consumerPig = object : Consumer<Pig> {
override fun consume(item: Pig) {
println("Consuming Pig")
}
}

val animals = listOf<Animal>(Pig(), Pig())
animals.consume(consumerPig) // Not complile: Type Mismatch
animals.consume(consumerAnimal) // alright
animals.consume(consumerCreature) // alright

By adding in notation , more general type becomes acceptable. This get rid of the second risk. That is it.


in and out are confusing sometimes, just like InputStream and OutputStream . You can simple consider using type in somewhat return statement as out , using type as args or parameter type as in . Ninth chapter of Kotlin in Action explained this in detail.

This post is mainly explain invariance, covariance and contravariance, and why this happen. For more detail , have a look at Kotlin in Action and Kotlin documentation.

CATALOG
  1. 1. Invariance, Why in and out mechanism
  2. 2. out, Covariance
  3. 3. in, Contravariance