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 that represent relationships like “A Cat IS-An Animal“. In Java, we can either use the extends keyword in order to change/expand behavior 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 type, 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(); //throws: java.lang.ArrayStoreException: java.lang.Object

Generic Collections

As of Java 1.5, Generics can be used in order to tell the compiler which elements are supposed to be stored in a particular collections instance (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 aren’t as flexible in regards to subtyping of collections at first.

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;

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 indicates 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, one of which can be declared with the generic type parameter ? super Animal (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 (again indicated by the wildcard). 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 can safely be added. This makes adding Cats as well as Dogs possible. 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 main part. Kotlin is different from Java when it comes to 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 the 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 wildcards 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 the assignment of 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 immediately understands: 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 – also called variance annotation. The compiler does not prohibit us to use T as a 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: Producer out, Consumer in (POCI)

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, similarly to Java, use-site variance which can be applied 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

In this article, I wanted to provide 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 improves and simplifies the usage of generics. In addition, the problem of covariant arrays is being eliminated. Allowing declaration-site variance simplifies a lot of client code, in 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 made a bit simpler in Kotlin.

I hope this brings some light into the darkness of generics and variance 🙂

More features of the fantastic Kotlin language are described in my other posts like the one on Vert.x. If you want to read about this topic in more detail, I highly 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 :)