Covariance, Contravariance, and Invariance — What do they mean? (Part 2)

In Kotlin, Java; and a little bit of generics too.

Ben Daniel A.
AndroidPub
4 min readMay 13, 2019

--

In the first part of this post, I gave a brief explanation and provided some examples on the different variance type systems. As promised, in this post we will go deeper into this topic. We will find out what these actually imply in the Java and Kotlin type systems.

To do this, I will set up a simple inheritance structure, just like we had before.

We will explore four different scenarios for each of the variance types, where it makes sense to: assignment statements, method overriding, arrays and generic collections.

Let’s begin.

Covariance in Java and Kotlin

Assignment statement: Variable assignment is covariant in nature in both Java and Kotlin. Therefore, the following Java lines in the java snippet are valid:

The same is valid for the Kotlin snippet too:

Method overriding: The behaviour for both languages are the same in this case too. The return types are both covariant and the method parameters are invariant. Consider this snippet:

This is a Kotlin snippet, it is similar in Java too.

Arrays: There is a difference in approach in this case for Java and Kotlin. Java goes the route of covariance while Kotlin sticks with invariance here. Consider the following snippet which are considered valid in Java:

However, this is not the case in Kotlin:

Generic Collections in Java: To make generic collections covariant in Java, we have to explicitly specify the variance type. This is called use-site variance. Consider the following snippet:

Now you might notice that there’s a potential ArrayStoreException (or similar exception) that can happen with the covariant generic collection. We are protected from this at compile time because the compiler will prevent us from calling any method that has the parameterised type as an argument. However we, can call any method that has the parameterised type as a return type.

Within the context of generic collections, this means that the list is now effectively a read-only list. This is because the ‘add(…)’ method in the List accepts the parameterised type and will cause a compile error if someone tries to call it. We can safely say that the list is a producer of generic elements.

Generic Collections in Kotlin: The covariance approach for generic collections is handled differently in Kotlin. Kotlin has a declaration-site variance feature that enables developers to define covariant generic types preemptively, this is done during the class definition.

Declaration-site variance

Unlike Java, Kotlin takes it a step forward, the compiler will not allow you to define methods in your class that have the parameterised type as an argument. If you can’t define them, you can’t call them.

If you can’t define them, you can’t call them.

If I add the following snippet into the body of the TestParameter class, then I won’t be able to compile my code:

There’s a way to suppress this error in IntelliJ as seen with the use of the @UnsafeVariance annotation in Kotlin’s Collections API. I would not recommend it.

There is also a fallback use-site variance feature for Kotlin in a case where it is not possible to use the declaration-site variance feature. For example if one is using a generic collection from an API that wasn’t created by oneself.

Assuming that I’m working with the class above and it exists in a third party API. I can still make the generic class covariant by using the following snippet:

We have looked at covariance in Java and Kotlin in this post, for sanity the next and final post in this series will be on contravariance and invariance in both languages.

Special thanks to this Stack Overflow answer, this medium post and this blog post. They gave me different perspectives for this series.

--

--