Item 24: Consider variance for generic types

Let’s say that we have the following generic class:

  1. class Cup<T>

Type parameter T in the above declaration does not have any variance modifier (out or in) and by default, it is invariant. It means that there is no relation between any two types generated by this generic class. For instance, there is no relation between Cup<Int> and Cup<Number>, Cup<Any> or Cup<Nothing>.

  1. fun main() {
  2. val anys: Cup<Any> = Cup<Int>() // Error: Type mismatch
  3. val nothings: Cup<Nothing> = Cup<Int>() // Error
  4. }

If we need such a relation, then we should use variance modifiers: out or in. out makes type parameter covariant. It means that when A is a subtype of B, and Cup is covariant (out modifier), then type Cup<A> is a subtype of Cup<B>:

  1. class Cup<out T>
  2. open class Dog
  3. class Puppy: Dog()
  4. fun main(args: Array<String>) {
  5. val b: Cup<Dog> = Cup<Puppy>() // OK
  6. val a: Cup<Puppy> = Cup<Dog>() // Error
  7. val anys: Cup<Any> = Cup<Int>() // OK
  8. val nothings: Cup<Nothing> = Cup<Int>() // Error
  9. }

The opposite effect can be achieved using in modifier, which makes type parameter contravariant. It means that when A is a subtype of B, and Cup is contravariant, then Cup<A> is a supertype of Cup<B>:

  1. class Cup<in T>
  2. open class Dog
  3. class Puppy(): Dog()
  4. fun main(args: Array<String>) {
  5. val b: Cup<Dog> = Cup<Puppy>() // Error
  6. val a: Cup<Puppy> = Cup<Dog>() // OK
  7. val anys: Cup<Any> = Cup<Int>() // Error
  8. val nothings: Cup<Nothing> = Cup<Int>() // OK
  9. }

Those variance modifiers are illustrated in the below diagram:

Item 24: Consider variance for generic types - 图1

Function types

In function types (explained deeply in Item 35: Consider defining a DSL for complex object creation) there are relations between function types with different expected types of parameters or return types. To see it practically, think of a function that expects as argument a function accepting an Int and returning Any:

  1. 1 fun printProcessedNumber(transition: (Int)->Any) {
  2. 2 print(transition(42))
  3. 3 }

Based on its definition, such a function can accept a function of type (Int)->Any, but it would also work with: (Int)->Number, (Number)->Any, (Number)->Number, (Any)->Number, (Number)->Int, etc.

  1. val intToDouble: (Int) -> Number = { it.toDouble() }
  2. val numberAsText: (Number) -> Any = { it.toShort() }
  3. val identity: (Number) -> Number = { it }
  4. val numberToInt: (Number) -> Int = { it.toInt() }
  5. val numberHash: (Any) -> Number = { it.hashCode() }
  6. printProcessedNumber(intToDouble)
  7. printProcessedNumber(numberAsText)
  8. printProcessedNumber(identity)
  9. printProcessedNumber(numberToInt)
  10. printProcessedNumber(numberHash)

It is because between those all types there is the following relation:

Item 24: Consider variance for generic types - 图2

Notice that when we go down in this hierarchy, the parameter type moves towards types that are higher in the typing system hierarchy, and the return type moves toward types that are lower.

Kotlin type hierarchy

It is no coincidence. All parameter types in Kotlin function types are contravariant, as the name of this variance modifier in suggests. All return types in Kotlin function types are covariant, as the name of this variance modifier out suggests

Item 24: Consider variance for generic types - 图4

This fact supports us when we use function types, but it is not the only popular Kotlin type with variance modifiers. A more popular one is List which is covariant in Kotlin (out modifier). Unlike MutableList which is invariant (no variance modifier). To understand why we need to understand the safety of variance modifiers.

The safety of variance modifiers

In Java, arrays are covariant. Many sources state that the reason behind this decision was to make it possible to create functions, like sort, that makes generic operations on arrays of every type. But there is a big problem with this decision. To understand it, let’s analyze following valid operations, which produce no compilation time error, but instead throws runtime error:

  1. // Java
  2. Integer[] numbers = {1, 4, 2, 1};
  3. Object[] objects = numbers;
  4. objects[2] = "B"; // Runtime error: ArrayStoreException

As you can see, casting numbersto Object[] didn’t change the actual type used inside the structure (it is still Integer), so when we try to assign a value of type String to this array, then an error occurs. This is clearly a Java flaw, and Kotlin protects us from that by making Array (as well as IntArray, CharArray, etc.) invariant (so upcasting from Array<Int> to Array<Any> is not possible).

To understand what went wrong here, we should first realize that when a parameter type is expected, we can pass any subtype of this type as well. Therefore when we pass an argument we can do implicit upcasting.

  1. open class Dog
  2. class Puppy: Dog()
  3. class Hound: Dog()
  4. fun takeDog(dog: Dog) {}
  5. takeDog(Dog())
  6. takeDog(Puppy())
  7. takeDog(Hound())

This does not get along with covariance. If a covariant type parameter (out modifier) was present at in-position (for instance a type of a parameter), by connecting covariance and up-casting, we would be able to pass any type we want. Clearly, this wouldn’t be safe since value is typed to a very concrete type and so when it is typed to Dog it cannot hold String.

  1. class Box<out T> {
  2. private var value: T? = null
  3. // Illegal in Kotlin
  4. fun set(value: T) {
  5. this.value = value
  6. }
  7. fun get(): T = value ?: error("Value not set")
  8. }
  9. val puppyBox = Box<Puppy>()
  10. val dogBox: Box<Dog> = puppyBox
  11. dogBox.set(Hound()) // But I have a place for a Puppy
  12. val dogHouse = Box<Dog>()
  13. val box: Box<Any> = dogHouse
  14. box.set("Some string") // But I have a place for a Dog
  15. box.set(42) // But I have a place for a Dog

Such a situation wouldn’t be safe, because after casting, the actual object stays the same and it is only treated differently by the typing system. We are trying to set Int, but we have only a place for a Dog. We would have an error if it was possible. This is why Kotlin prevents such a situation by prohibiting using covariant (out modifier) type parameters at a public in-position.

  1. class Box<out T> {
  2. var value: T? = null // Error
  3. fun set(value: T) { // Error
  4. this.value = value
  5. }
  6. fun get(): T = value ?: error("Value not set")
  7. }

It is fine when we limit visibility to private because inside the object we cannot use covariance to up-cast object:

  1. class Box<out T> {
  2. private var value: T? = null
  3. private set(value: T) {
  4. this.value = value
  5. }
  6. fun get(): T = value ?: error("Value not set")
  7. }

Covariance (out modifier) is perfectly safe with public out-positions and so they are not limited. This is why we use covariance (out modifier) for types that are produced or only exposed. It is often used for producers or immutable data holders.

One good example is a List<T> in which T is covariant in Kotlin. Thanks to that when a function expects List<Any?>, we can give any kind of list without any transformation needed. In MutableList<T>, T is invariant because it is used at in-position and it wouldn’t be safe:

  1. fun append(list: MutableList<Any>) {
  2. list.add(42)
  3. }
  4. val strs = mutableListOf<String>("A", "B", "C")
  5. append(strs) // Illegal in Kotlin
  6. val str: String = strs[3]
  7. print(str)

Another good example is Response which can benefit a lot from using it. You can see how they can be used in the following snippet. Thanks to the variance modifiers, the following desired facts come true:

  • When we expect Response<T>, response with any subtype of T will be accepted. For instance, when Response<Any> is expected, we accept Response<Int> as well as Response<String>.
  • When we expect Response<T1, T2>, response with any subtype of T1 and T2 will be accepted.
  • When we expect Failure<T>, failure with any subtype of T will be accepted. For instance, when Failure<Number> is expected, Failure<Int> or Failure<Double> are accepted. When Failure<Any> is expected, we accept Failure<Int> as well as Failure<String>.
  • Success does not need to specify a type of potential error, and Failure does not need to specify a type of potential success value. This is achieved thanks to covariance and Nothing type.
  1. sealed class Response<out R, out E>
  2. class Success<out R>(val value: R): Response<R, Nothing>()
  3. class Failure<out E>(val error: E): Response<Nothing, E>()

A similar problem as with covariance and public in-positions occurs when we are trying to use a contravariant type parameter (in modifier) as a public out-position (return type of a function or a property type). Out-positions also allow implicit up-casting.

  1. open class Car
  2. interface Boat
  3. class Amphibious: Car(), Boat
  4. fun getAmphibious(): Amphibious = Amphibious()
  5. val car: Car = getAmphibious()
  6. val boat: Boat = getAmphibious()

This fact does not get along with contravariance (in modifier). They both can be used again to move from any box to expect anything else:

  1. class Box<in T>(
  2. // Illegal in Kotlin
  3. val value: T
  4. )
  5. val garage: Box<Car> = Box(Car())
  6. val amphibiousSpot: Box<Amphibious> = garage
  7. val boat: Boat = garage.value // But I only have a Car
  8. val noSpot: Box<Nothing> = Box<Car>(Car())
  9. val boat: Nothing = noSpot.value
  10. // I cannot produce Nothing!

To prevent such a situation, Kotlin prohibits using contravariant (in modifier) type parameters at public out-positions:

  1. class Box<in T> {
  2. var value: T? = null // Error
  3. fun set(value: T) {
  4. this.value = value
  5. }
  6. fun get(): T = value // Error
  7. ?: error("Value not set")
  8. }

Again, it is fine when those elements are private:

  1. class Box<in T> {
  2. private var value: T? = null
  3. fun set(value: T) {
  4. this.value = value
  5. }
  6. private fun get(): T = value
  7. ?: error("Value not set")
  8. }

This way we use contravariance (in modifier) for type parameters that are only consumed or accepted. One known example is kotlin.coroutines.Continuation:

  1. public interface Continuation<in T> {
  2. public val context: CoroutineContext
  3. public fun resumeWith(result: Result<T>)
  4. }

Variance modifier positions

Variance modifiers can be used in two positions. The first one, the declaration-side, is more common. It is a modifier on the class or interface declaration. It will affect all the places where the class or interface is used.

  1. // Declaration-side variance modifier
  2. class Box<out T>(val value: T)
  3. val boxStr: Box<String> = Box("Str")
  4. val boxAny: Box<Any> = boxStr

The other one is the use-site, which is a variance modifier for a particular variable.

  1. class Box<T>(val value: T)
  2. val boxStr: Box<String> = Box("Str")
  3. // Use-side variance modifier
  4. val boxAny: Box<out Any> = boxStr

We use use-site variance when for some reason we cannot provide variance modifiers for all instances, and yet you need it for one variable. For instance, MutableList cannot have in modifier because then it wouldn’t allow returning elements (as described in the next section), but for a single parameter type we can make its type contravariant (in modifier) to allow any collections that can accept some type:

  1. interface Dog
  2. interface Cutie
  3. data class Puppy(val name: String): Dog, Cutie
  4. data class Hound(val name: String): Dog
  5. data class Cat(val name: String): Cutie
  6. fun fillWithPuppies(list: MutableList<in Puppy>) {
  7. list.add(Puppy("Jim"))
  8. list.add(Puppy("Beam"))
  9. }
  10. val dogs = mutableListOf<Dog>(Hound("Pluto"))
  11. fillWithPuppies(dogs)
  12. println(dogs)
  13. // [Hound(name=Pluto), Puppy(name=Jim), Puppy(name=Beam)]
  14. val animals = mutableListOf<Cutie>(Cat("Felix"))
  15. fillWithPuppies(animals)
  16. println(animals)
  17. // [Cat(name=Felix), Puppy(name=Jim), Puppy(name=Beam)]

Notice that when we use variance modifiers, some positions are limited. When we have MutableList<out T>, we can use get to get elements and we receive an instance typed as T, but we cannot use set because it expects us to pass an argument of type Nothing. It is because list with any subtype of T might be passed there including the subtype of every type that is Nothing. When we use MutableList<in T>, we can use both get and set, but when we use get, the returned type is Any? because there might be a list with any supertype of T including the supertype of every type that is Any?. Therefore, we can freely use out when we only read from a generic object, and in when we only modify that generic object.

Summary

Kotlin has powerful generics that support constraints and also allow to have a relation between generic types with different type arguments declared both on declaration-side as well as on use-side. This fact gives us great support when we operate on generic objects. We have the following type modifiers:

  • The default variance behavior of a type parameter is invariance. If in Cup<T>, type parameter T is invariant and A is a subtype of B then there is no relation between Cup<A> and Cup<B>.
  • out modifier makes type parameter covariant. If in Cup<T>, type parameter T is covariant and A is a subtype of B, then Cup<A> is a subtype of Cup<B>. Covariant types can be used at out-positions.
  • in makes type parameter contravariant. If in Cup<T>, type parameter T is contravariant and A is a subtype of B, then Cup is a subtype of Cup. Contravariant types can be used at in-positions.

In Kotlin:

  • Type parameter of List and Set are covariant (out modifier) so for instance, we can pass any list where List<Any> is expected. Also, the type parameter representing value type in Map is covariant (out modifier). Type parameters of Array, MutableList, MutableSet, MutableMap are invariant (no variance modifier).
  • In function types parameter types are contravariant (in modifier) and return type is covariant (out modifier).
  • We use covariance (out modifier) for types that are only returned (produced or exposed).
  • We use contravariance (in modifier) for types that are only accepted.