Understanding Generics and Variance in Kotlin

Tomek Polański
ProAndroidDev
Published in
6 min readJul 7, 2017

--

One of the biggest selling points of Object-Oriented Programming languages is inheritance.

It allows us to do things like assigning a subclass instance to superclass:

Integer integer = new Integer(1);
Number number = integer;

And thanks to it we can write our code in a more abstract way by declaring a method:

void printNumber(Number number)

Which we can use with both types:

printNumber(integer);
printNumber(number);

Unfortunately, with generics in Java it is not always so straightforward:

List<Integer> integerList = new ArrayList<>();
List<Number> numberList = integerList; // Compiler error

To truly understand generics, first, we need to understand three main concepts:

  • class vs type
  • subclass vs subtype
  • variance: covariance, contravariance and invariance

Class vs type

You might not have thought about classes and types as distinct concepts.

In Java and Kotlin all classes have at least one type that is the same as the class, for example, an Integer is a class and at the same time a type.

On the other hand, in Kotlin we also have nullable types, consider String?` we can’t really say that String? is a class, because notionally, it is still a String.

Another example in Java and Kotlin is List. List is a class, but List<String> is not a class, it’s a type.

The following table summarizes these examples:

Class vs type

Subclass vs subtype

For a class to be a subclass of another class, it needs to inherit from it. For example, Integer inherits from Number, so Integer is a subclass of Number.

Number to Integer relation

That also means that we can make the following assignment:

Java:

Integer integer = new Integer(1);
Number number = integer;

Kotlin:

val integer: Int = 1
val number: Number = integer

This is possible because Integer is a subtype of Number.

But let’s try the same with nullable type:

val integer: Int = 1;
val nullableInteger: Int? = integer;

Here Int is a subtype of nullable Int so we can assign Int to Int?.

Nullable Int to Int relation

Variance

First, let’s define a few Kotlin classes:

abstract class Animal(val size: Int)
class Dog(val cuteness: Int): Animal(100)
class Spider(val terrorFactor: Int): Animal(1)
Subtypes of Animal

Now let’s check if Dog and Spider are subtypes of Animal:

val dog: Dog = Dog(10)
val spider: Spider = Spider(9000)
var animal: Animal = dog
animal = spider

It works nicely: no compiler error. Try it for yourself!

Covariance

Let’s use some more complex types by wrapping our types into generic Lists.

One important thing to remember, this list is an immutable List from Kotlin: you are not able to modify its contents after you create it. You will find out why this matters shortly.

val dogList: List<Dog> = listOf(Dog(10), Dog(20))
val animalList: List<Animal> = dogList

Variance tells us about the relationship between List<Dog> and List<Animal> where Dog is a subtype of Animal.

In Kotlin, dogList can be assigned to Animal list (val animalList: List<Animal> = dogList) so the type relation is preserved and List<Dog> is a subtype of List<Animal>. This is called covariance.

Covariance

Invariance

In Java, even though Dog is a subtype of Animal, you cannot do the following:

List<Dog> dogList= new ArrayList<>();
List<Animal> animalList = dogList; // Compiler error

This is because generics in Java ignore type vs subtype relation between its components. In the case when List<Dog> cannot be assigned to List<Animal> nor vice versa, this is called invariance. There is no subtype to supertype relationship here.

Invariance

Contravariance

Perhaps we want to compare our animals, that’s why we can create an interface Compare<T>, with a method compare(T item1, T item2) and that method can say which item is first and which item is second.

Whenever we compare dogs we look how cute the dogs are, here is code for comparing dogs:

val dogCompare: Compare<Dog> = object: Compare<Dog> {
override fun compare(first: Dog, second: Dog): Int {
return first.cuteness - second.cuteness
}
}

What will happen if we would try to assign this compare mechanism to animal comparator:

val animalCompare: Compare<Animal> = dogCompare // Compiler error

There is a really good reason why this does not work. If it worked, we would be able to pass spiders to animalCompare, but this would be an error because dogCompare can only compare dogs and not spiders.

On the other hand, if we would have a way to compare all the animals, that mechanism ought to work for dogs and spiders:

val animalCompare: Compare<Animal> = object: Compare<Animal> {
override fun compare(first: Animal, second: Animal): Int {
return first.size - second.size
}
}
val spiderCompare: Compare<Spider> = animalCompare // Works nicely!

We can see that Spider is a subtype of Animal, but Compare<Animal> is a subtype of Compare<Spider> — the type relation is reversed. It’s also known as contravariance.

Contravariance

Java vs Kotlin

Thankfully in Java, we are able to make generics covariant or contravariant.

Java

To make a generic type covariant you just need to write:

List<Dog> dogs = new ArrayList<>();
List<? extends Animal> animals = dogs;

extends makes the relation between animals and dogs list types covariant.

For contravariant you can write:

Compare<Animal> animalCompare = (first, second) -> first.getSize() — second.getSize();
Compare<? super Spider> spiderCompare = animalCompare;

super makes animal and spider compare contravariant.

This way of creating type variance at their point of use is called use-site variance.

Kotlin

In Kotlin there is no magic (at least officially proven), it also has a way to define generics to be covariant or contravariant.

Out

If you look into the definition of the immutable list in Kotlin you will see this line:

interface List<out E> {
fun get(index: Int): E
}

The out keyword says that methods in a List can only return type E and they cannot take any E types as an argument.

This limitation allows us to make List covariant.

in

When you check the definition of Compare you can see something different:

interface Compare<in T> {
fun compare(first: T, second: T): Int
}

In this case, there is in keyword next to the parameter.

This means that all methods in Compare can have T as an argument but cannot return T type.

This makes Compare contravariant.

In Kotlin, the developer that writes the class declaration needs to consider the variance and not the developer who uses the code. This is why it is called declaration-site variance.

Variance

Now you should know what kind of generic class you need to declare(otherwise ask me in the comments).

If you’d like to check out the whole examples, here you can find them.

In case you want to learn more about generics, check out Kotlin’s documentation page. I also highly recommend book Kotlin in Action which is written by JetBrains people.

--

--

Passionate mobile developer. One thing I like more than learning new things: sharing them