Author profile picture

Invariance

When dealing with simple classes, subtyping is straightforward. But once we start talking about generics, subtyping rules become more complicated, and we describe the subtyping rules in terms of variance.

What is Invariance?

Invariance describes a relationship between two sets of types where the complex types do not subtype at all, despite any subtyping that might exist for the component types.

Does that sound confusing?1 Thankfully, it’s way more easily demonstrated than defined, so let’s look at an example. We’ll start off with a simple inheritance hierarchy:

open class A
open class B : A()

Here we’ve just got two classes, where B is a subtype of A. That’s our first set of types.

For our second set of types, let’s consider these generic classes:

MutableList<A>
MutableList<B>

So, we’ve got two sets of types - the MutableList types, which are complex, consisting of component types of A and B.

The subtyping is straightforward for A and B, but what’s the nature of the relationship between MutableList<A> and MutableList<B>?

  • Is MutableList<B> a subtype of MutableList<A>?
  • Is MutableList<A> a subtype of MutableList<B>?
UML diagram depicting subtyping of A and B. What is the subtyping for the MutableLists?

The answer: neither one is a subtype of the other, so we say that MutableList is invariant on its type parameter.

Examples

Single Type Parameter

By default, generics in Kotlin (and Java) are invariant – if you don’t specify any variance annotation, like in or out, a generic class will be invariant. For example:

class Box<T>

In this class, T is preceeded by neither in nor out, so Box is invariant on type parameter T.

The benefit to invariance is that the type parameter (such as T in the example above) can be used in both the “in” and “out” positions in the class. In other words, it can appear as a function argument (going “in” to a function) and can be returned from a function (coming “out” of a function).

Multiple Type Parameters

It’s possible for a class to exhibit different variance on different type parameters:

class ComplicatedBox<out T, in U, V>

In this case:

  • ComplicatedBox is covariant on type parameter T
  • ComplicatedBox is contravariant on type parameter U
  • ComplicatedBox is invariant on type parameter V

Invariance and Inheritance

Variance doesn’t just apply to generics - it applies to property types, function argument types, and function result types of a class when it is inherited. For example:

open class Supertype {
    open var property: A = A()
    open fun execute(item: B): A = prop
}

If a subclass were to inherit from Supertype, then could that subclass declare its property to be of type B instead of A? What about execute’s argument or return type? Could it declare item as A? Or declare the return type as B?

  • Properties are invariant when they are declared with var, because it effectively results in both a getter and a setter for the property, which puts the type in both the “in” (setter) and “out” (getter) positions.
  • In Kotlin and Java, function arguments are invariant. If a subclass specifies a more general argument type than its superclass, the function will be overloaded, not overridden. That is, the subclass will have two functions with the same name, expecting different argument types. (But there’s a neat trick you can do to allow for contravariant argument types – see the section Contravariance and Inheritance in the Contravariance article for details.)
  • Function return types are not invariant - they’re covariant, so a subclass can declare a return type that’s more specific than its superclass.

More Information

For more information on variance, check out these articles:

Or, go read up on variance in Kotlin’s official documentation.


  1. This was my best take at a definition after a ridiculous amount of time crafting it. I’m open to revisions, so if you’ve got a better way to define this in a single sentence, I’d love to hear about it in the comments section below. [return]

Share this article: