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:
- A negative number if the receiver sorts before the other value
- A positive number if the other value sorts before the receiver
- Zero if the two values are equal
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.