Contravariance in Generics

Continuing our examination of type inheritance and their impacts on generics… sometimes, we like to sort animals.

One option is to use Comparable and have Animal know how to compare itself to other Animal objects:

open class Animal : Comparable<Animal> {
  override fun compareTo(other: Animal) = toString().compareTo(other.toString())
}

class Frog : Animal() {
  override fun toString() = "Frog!"
}

class Axolotl : Animal() {
  override fun toString() = "Axolotl?"
}

fun main() {
  println(listOf(Frog(), Axolotl()).sorted())
}

compareTo() works as it does in Java: it needs to return:

This works well, sorting our Axolotl before our Frog:

[Axolotl?, Frog!]

And, since our list contains a Frog and an Axolotl, Kotlin is going to to find the common supertype — Animal — and treat our list as a List<Animal>.

We could also use Comparator, which has a compare() function that works like compareTo(). However, Comparator is a standalone interface, knowing how to compare two objects of some generic type:

open class Animal

class Frog : Animal() {
  override fun toString() = "Frog!"
}

class Axolotl : Animal() {
  override fun toString() = "Axolotl?"
}

class FrogComparator : Comparator<Frog> {
  override fun compare(one: Frog, two: Frog) =
    one.toString().compareTo(two.toString())
}

fun main() {
  println(listOf(Frog(), Frog()).sortedWith(FrogComparator()))
}

This works, because we are sorting a List<Frog> and FrogComparator knows how to compare frogs. But, suppose we pass in the same frogs… as a List<Animal>:

open class Animal

class Frog : Animal() {
  override fun toString() = "Frog!"
}

class Axolotl : Animal() {
  override fun toString() = "Axolotl?"
}

class FrogComparator : Comparator<Frog> {
  override fun compare(one: Frog, two: Frog) =
    one.toString().compareTo(two.toString())
}

fun main() {
  println(listOf<Animal>(Frog(), Frog()).sortedWith(FrogComparator()))
}

Now, we have a compile error, because even though the underlying objects are of type Frog, we are treating them as Animal, and FrogComparator does not know how to compare Animal objects.

So, why can we sort a List<Animal> using Comparable but not Comparator? They are declared slightly differently, just as List and MutableList were in the preceding chapter.

Comparator works simply on a generic type T:

fun interface Comparator<T>

…while Comparable uses a new keyword, in:

interface Comparable<in T>

in indicates that our primary use of the data type is to accept it as input, in ways where we can work with sub-types for that input. In other words, Comparable<Animal> can compare a Frog or an Axolotl, as those are sub-types of Animal.

Using the plain generic type, as Comparator does, means that Compartor is invariant with respect to the type. Conversely, the use of in by Comparable means that Comparable is contravariant with respect to the type.

Declaration-Site and Use-Site Variance

As with out, in can be used in a declaration (as Comparable does) or at a use site (e.g., a function parameter).

For example, suppose that we want to replace a frog with an axolotl in a list:

open class Animal

class Frog : Animal() {
  override fun toString() = "Frog!"
}

class Axolotl : Animal() {
  override fun toString() = "Axolotl?"
}

fun replaceWithAnimal(list: MutableList<Animal>, value: Animal) {
  for (i in list.indices) {
    list[i] = value
  }
}

fun main() {
  val animals: MutableList<Any> = mutableListOf(Frog(), Frog())
  val replacement = Axolotl()
  
  replaceWithAnimal(animals, replacement)
  
  println(animals)
}

animals is a MutableList<Any>. So, even though it points to an object that contains only frogs, we are treating it as though it is a list that contains any possible type. This is a problem when we call replaceWithAnimal(), as it is expecting a MutableList<Animal>, not a MutableList<Any>.

The use of in changes that:

open class Animal

class Frog : Animal() {
  override fun toString() = "Frog!"
}

class Axolotl : Animal() {
  override fun toString() = "Axolotl?"
}

fun replaceWithAnimal(list: MutableList<in Animal>, value: Animal) {
  for (i in list.indices) {
    list[i] = value
  }
}

fun main() {
  val animals: MutableList<Any> = mutableListOf(Frog(), Frog())
  val replacement = Axolotl()
  
  replaceWithAnimal(animals, replacement)
  
  println(animals)
}

At a use site, in says “we accept this type or any supertype”. So, a MutableList<Any> satisfies MutableList<in Animal>. And, logically, this makes sense: we are attempting to replace an element of the list with an Animal, and that works whether the list is a list of Animal or a list of Any.

Note, though, that we are also relying on Kotlin’s type inference. mutableListOf(Frog(), Frog()), by default, would return a MutableList<Frog>. But we are not stating that explicitly. Kotlin sees that we are using it to initialize a variable of type MutableList<Any>, so it decides that we really wanted the list to actually be a MutableList<Any>. If we force it to be a MutableList<Frog>:

val animals: MutableList<Any> = mutableListOf<Frog>(Frog(), Frog())

…then we have a covariance problem. MutableList is invariant in its type, so we cannot assign a MutableList<Frog> to a MutableList<Any>.

And, if we turn around and say that animals is supposed to be a MutableList<Frog>:

val animals: MutableList<Frog> = mutableListOf(Frog(), Frog())

…then we have another covariance problem. replaceWithAnimal() will accept a MutableList of Animal or a supertype, but not sub-type. Hence, it will not accept a MutableList<Frog>, which makes sense, as we cannot assign an Animal to a slot in a list that only accepts Frog.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.