Author profile picture

Covariance

On the surface, subtypes seem straightforward. Most programmers who write object-oriented code are familiar with the concept of inheritance, and how you can pass off a subclass as if it were its superclass. But subtyping rules aren’t always as intuitive when you start working with generics.

What is Covariance?

Covariance describes a relationship between two sets of types where they both subtype in the same direction. For example, for our first set of types, here are two classes, A and B, where B is a subtype of A:

open class A
open class B : A()

Simple.

And for our second set of types, we’ll consider a read-only List of each of those two classes:

List<A>
List<B>

The subtyping rules for A and B are obvious. But what are the subtyping rules for List<A> and List<B>?

UML diagram depicting subtyping of A and B. What is the subtyping for the Lists?

Is List<B> a subtype of List<A>, just like B is a subtype of A? If so, then the subtypes go in the same direction, and we say that List is “covariant on its type parameter”.

And in case you’re wondering, in Kotlin:

  • A read-only List is indeed covariant on its type parameter. So List<B> is a subtype of List<A>.
  • A MutableList, however, is not covariant, so MutableList<B> is not a subtype of MutableList<A>.

Examples

Using covariance

Because List is covariant, you can pass off a read-only List<B> as if it were List<A>:

val listOfB: List<B> = listOf(B())
val listOfA: List<A> = listOfB

This also works when passing a List to a function:

fun read(list: List<A>) {
    // ...
}

val list: List<B> = listOf(B())
read(list)

Creating covariance

Covariance won’t happen automatically on just any ol’ generic class. For example, here’s a simple generic Box class that wraps… well… anything that’s not null:

class Box<T>(val item: T)

Box is not covariant on T in this case, so the following will fail compilation with a Type mismatch error on the last line:

fun read(box: Box<A>) {
    // ...
}

val box: Box<B> = Box(B())
read(box)

In order to make it covariant, you’ll need to mark the type parameter as out:

class Box<out T>(val item: T)

The code above will now compile.

What’s the downside? By marking a type parameter with the out variance annotation, you’re also telling the compiler that, once an object of this type is constructed, it will never accept a function argument of type T, because doing so would not be type-safe. This is why the type parameter is marked as out - because the class will only ever send a T out (i.e., return it from a function).

Note that this also applies to getters and setters of properties. For example, if we were to change the constructor to use var item instead of val item, the code would not compile due to the type variance conflict, and you’d get an error message that looks like this:

Type parameter T is declared as ‘out’ but occurs in ‘invariant’ position in type T

Source

Covariance and Inheritance

Covariance doesn’t just apply to generics. When you override a function in a subclass, you can declare a return type that’s more specific than the superclass declares. For example, if you’ve got a superclass with a function that returns an A, it’s legal for a subclass to declare that it returns a B instead:

open class SuperClass {
    open fun getValue(): A {
        return A()
    }
}

class SubClass : SuperClass() {
    override fun getValue(): B {
        return B()
    }
}

This is because return types in Kotlin (and Java) are covariant.

More Information

More concept articles related to variance:

Share this article:

  • Share on Twitter
  • Share on Facebook
  • Share on Reddit