Generic Types and Variance in Kotlin compared to Java

Generic Types and Variance in Kotlin compared to Java

Basics – What is Variance?

Many programming languages support the concept of subtyping, which allows us to implement hierarchies like “A Cat IS-An Animal“. In Java we can either use the extends keyword in order to change/expand behaviour of an existing class (inheritance) or use implements to provide implementations for an interface. According to Liskov’s substitution principle, every instance of a class A can be substituted by instances of its subtype B. The word variance, often referred to in mathematics as well, is used to describe how subtyping in complex aspects like method return types, type declarations, generic types or arrays relates to the direction of inheritance of the involved classes. There are three terms we need to take into account: Covariance, Contravariance and Invariance.

Variance in Practice (Java)

Covariance

In Java an overriding method needs to be covariant in its return types, i.e. the return types of the overridden and the overriding method must be in line with the direction of the inheritance tree of the involved classes. A method treat():Animal of class AnimalDoctor can be overridden with treat():Cat in class CatDoctor, which extends AnimalDoctor. Another example of covariance would be type conversion, shown here:

public class Animal{}
public class Cat extends Animal{}

(Animal) new Cat() //works fine
(Cat) new Animal() //will not work

Subclasses can be cast up the inheritance tree, while downcasting will cause an error here. This is also the case if we take a look at variable declarations. It isn’t a problem to assign an instance of Cat to a variable of type Animal, whereas doing the opposite will cause failure.

Contravariance

Contravariance on the other hand describes the exact opposite. In Java this concept only reveals itself when we work with generics, which I’m going to describe in-depth later. Just to make it clear, we can image another programming language that allows contravariant method arguments in overriding methods [1]. Let’s say we have a class ClassB which extends another class ClassA and overrides a method by changing the original parameter type T' to its supertype T.

ClassA::method(t:T')

ClassB::method(t:T)

You can see that the type hierarchy of method parameter t is contrary to the hierarchy of the surrounding classes. Up versus down the tree, method is contravariant in its parameter type.

Invariance

Last but not least, the easiest one: Invariance. It can be observed when we think of overriding methods in Java again like we’ve just seen in the example before. An overriding method must accept just the same parameters as the overridden method. This means we speak of invariance if the types of an aspect like “method parameters” do not differ in super- and subtype.

Variance of collection types in Java

Another aspect we want to consider is arrays and other kinds of generic collections.

Arrays in Java are covariant in their type, which means an array of Strings can be assigned to a variable of type “Array of Object“.

Array Variance
Object [] arr = new String [] {"hello", "world"};

Also, arrays are covariant in the types that they hold. This means you can add Integers, Strings or whatever kind of Object to an Object [].

Object [] arr = new Object [2];
arr[0] = 1;
arr[1] = "2";

This seems to be quite handy but can cause errors at runtime. Looking at example Array Variance again: The variable is of type Object [] but the referenced object is a String [].
What happens if we pass the variable to a method expecting an array of Objects? This method might want to add an Object to the array, which seems legit because the parameter is expected to be of type Object []. It will cause an ArrayStoreException at runtime, easily shown here:

Array Runtime Error
Object [] arr = new String [] {"hello", "world"};
arr[1] = new Object(); //will throw Exception; java.lang.ArrayStoreException: java.lang.Object

Generic Collections

As of Java 1.5 we can use Generics in order to tell the compiler which elements are supposed to be stored in our collections (i.e. List, Map, Queue, Set). Unlike arrays, generic collections are invariant in their parameterized type by default. This means you can’t substitute a List<Animal> with a List<Cat>. It won’t even compile. As a result it is not possible to run into unexpected runtime errors like it is when working with covariant arrays. As a drawback, we are much more inflexible regarding subtyping of collections.

Fortunately, the user can specify the variance of type parameters himself when using generics, which we call use-site variance.

Covariant collections

The following code example shows how we declare a covariant list of Animal and assign a list of Cat to it.

List<Cat> cats = new ArrayList<>();
List<? extends Animal> animals = cats;

Anyways, such a covariant list is still different to an array, because the covariance is encoded in its type parameter. We can only read from the list, whereas adding is prohibited. The list is said to be a Producer of Animals. The generic type ? extends Animal [2] only indicated that the list contains any type with an upper bound of Animal, which could mean a list of Cat, Dog or any other animal. This approach turns the runtime error encountered in Array Runtime Error into a compile error.

List<Cat> cats = new ArrayList<>();
List<? extends Animal> animals = cats;
animals.add(new Cat()); //will not compile
Contravariant collections

It is also possible to work with contravariant collections. Such a list can be declared with the generic type parameter ? super Animal, which means a lower bound of type Animal. Such a list may be of type List<Animal> itself or a list of any super type of Animal, even Object.
Like with covariant lists, we do not know for sure which type the list really represents (indicated by the wildcard again). The difference is, we can not read from a contravariant list, since it is unclear if we will get Animals or just plain Objects. But now we can write to the list as we know that at least Animals may be added. This allows us to safely add Cats as well as Dogs. Such a list is said to be a Consumer.

List<Animal> animals = new ArrayList<>();
List<? super Animal> contravariantAnimals = animals;
contravariantAnimals.add(new Cat());
contravariantAnimals.add(new Dog());
Animal pet = contravariantAnimals.get(0); // will not compile

Joshua Bloch created a rule of thumb in his fantastic book Effective Java:
“Producer-extends, consumer-super (PECS)”

Variance of generic types in Kotlin

After we’ve seen what variance in general means and how Java makes use of these concepts, I’d like to get to this blog post’s point. Kotlin is different from Java regarding generics and also arrays in a few ways and it might look odd to an experienced Java developer at first glance.

The first and maybe easiest difference is: Arrays in Kotlin are invariant. This means, as opposed to Java, it is not possible to assign an Array<String> to a reference variable of type Array<Object>. This ensures compile time safety and prevents runtime errors like you may encounter in Java with its covariant arrays. But is there a way to safely work with subtyped arrays? Sure, there is – we’ll look at it next.

Declarion-site Variance

As we’ve seen, Java uses so-called “wildcard types” to make generics variant, which is said to be “the most tricky part[s] of Java’s type system” [3]. The whole thing is called “use-site variance”. Kotlin does not use these at all. Instead, in Kotlin we use declaration-site variance. Let’s recall the initial problem again: Let’s imagine, we have a class ReadableList<E> with one simple producer method get():T. Java prohibits to assign an instance of ReadableList<String> to a variable of type ReadableLis<Object> because generic types are invariant by default. To fix this, the user can change the variable type to ReadableList<? extends Object> and everything works fine. Kotlin approaches this problem in a different way. The type T can be marked as ‘only produced’ with the out keyword, so that the compiler instantly gets it: ReadableList is never gonna consume any T, which makes T covariant.

Kotlin out
abstract class ReadableList<out T> {
    abstract fun get(): T
}

fun workWithReadableList(strings: ReadableList<String>) {
    val objects: ReadableList<Any> = strings // This is OK, since T is an out-parameter
    // ...
}

As you can see in “Kotlin out” the type T is annotated as an out type via declaration-site variance – it is also referred to as variance annotation. The compiler does not prohibit us to use T as an covariant type. Of course there is also a complementary annotation to mark generic types as consumers, i.e. makes them contravariant: in. This approach has also been used in C# successfully for some years already.

The Kotlin rule to memorize: Consumer in, Producer out (CIPO)

Use-site Variance, Type projections

Unfortunately, it is not always sufficient to have the opportunity of declaring a type parameter T as either out or in. Just think of arrays for example. An array offers methods for adding and receiving objects, so it cannot be either covariant or contravariant in its type parameter. As an alternative Kotlin also allows use-site variance, which is very similar to Java using the already defined keywords in and out:

  • Array<in String> corresponds to Java’s Array<? super String>
  • Array<out String> corresponds to Java’s Array<? extends Object>
Type projection Example
fun copy(from: Array<out Any>, to: Array<Any>) {
 // ...
}

This example shows how from is declared as a consumer of its type and thus the method cannot do ‘bad things’ like adding to the Array. This concept is called type projection since the array is restricted in its methods: only those methods that return type parameter T may be called.

Bottom line

With this post I wanted to provide same basic information on the quite complex aspects of variance in the context of generics. Mostly, Java was used to demonstrate the concepts of co-, contra- and invariance, which are hard to understand in connection with Java’s wildcard types. I’ve shown how Kotlin tries to simplify the whole thing using different approaches (declaration-site variance) and more obvious keywords (in, out).

In my opinion Kotlin really makes it better than Java and also eliminates another problem, which is covariant arrays. Allowing declaration-site variance simplifies a lot of client code where it’s not necessary to use complex declarations like we’re used to in Java. Also even if we have to fall back on use-site variance its a bit simpler.

I hope this makes sense to you 🙂

More features of Kotlin are described in my Post on Vert.x.

If you want to read about this topic in more detail I recommend the book Kotlin in Action to you!


1. In Java such a method would be treated as overloaded
2. ? is the “wildcard” character

Please follow and like me 🙂

One Reply to “Generic Types and Variance in Kotlin compared to Java”

Leave a Reply

Enjoy this blog? Please spread the word :)