Skip to content

Instantly share code, notes, and snippets.

@darrenbkl
Created January 22, 2020 11:31
Show Gist options
  • Save darrenbkl/373d0b09c62fee569e74c26135f694e1 to your computer and use it in GitHub Desktop.
Save darrenbkl/373d0b09c62fee569e74c26135f694e1 to your computer and use it in GitHub Desktop.

Covariance and Contravariance

As Software Engineer, the concept of variance is something that we deal with almost on a daily basis, mostly unconsciously. Understanding it can help you to write generics code mindfully, and view library code in a new light.

Gentle Introduction

Let’s look at this code https://gist.github.com/60d4bdacfdcd96356d2afe0901a8ee7a

The code above will compile successfully. However, when you run the program, you will get this https://gist.github.com/15ad02d623a97f21628d42a8c7b35207

Observations

  1. You can assign an Integer[] to a Number[], just like you can Number number = new Integer(1).
  2. At runtime, Java knows that numArray is an Integer[] .
  3. You can add a Double to numArray with no compilation error, but at runtime, you get ArrayStoreException.

Explanation

  1. Arrays are covariant.
  2. Array is a Reifiable Type (link to previous post). After compilation, numArray is reified to Integer[].
  3. At compile-time, numArray is just a Number[], that’s why you can add a Double (subtype of Number) to it. However, at runtime, since numArray is reified to Integer[], it should not contain a Double.

numArray is like a double agent, it acts as a Number[] at compile time, but as a Integer[] at runtime, fooling you into give it more things that you should. Java array is flawed, but this is whole topic for another post. The important thing that we’ve learned is that arrays are covariant.

Arrays are Covariant

Covariance is defined as follow

If x is a subtype of y, then f(x) is a subtype of f(y) A covariant function/type preserves the sub typing relation of x and y.

So, when we say Arrays are Covariant, we are basically saying that

If x is a subtype of y, then x[] is a subtype of y[] In the example above, Integer is a subtype of Number, so Integer[] is a subtype of Number[].

Contravariance

Since we are talking about Covariance, let’s also take a look at the Contravariance. Contravariance is the opposite of Covariance, it is defined as follow

If x is a subtype of y, then f(y) is a subtype of f(x) A contravariant function/type reverses the subtyping relation of x and y. Array is not contravariant, because a Number[] is not subtype of Integer[].

Array allows unsafe operations, as demonstrated above. Let’s take a look at how a generic List overcomes this with invariance.

Generics are Invariant

A generic type like List, is invariant.

Invariance disregard the subtyping relation, it is neither covariant or contravariant. List must be exactly a List. A List is not a List, vice versa. https://gist.github.com/34f91ecc0548357ea07874fc4c413c6b

The implication is invariance is that it is impossible to add a subtype of T to a List. This solves the ArrayStoreException issue that we encountered with array. When trying to add incompatible types, with array, you only get ArrayStoreException at runtime, but with List, you get compilation error immediately, which is good. https://gist.github.com/f8f1a32bab53ac7bfbffd0b461ca0778

This is cool, but an invariant list is quite limited, what if we want a function to accept a list of any subtypes of Number? We can’t do that with List because it won’t work with subtypes due to invariance.

Make Generics Flexible

We can use bounded wildcard to allow subtyping flexibility when writing generic code. extends can be used to make the List covariant. We can think of List<? extends Numbers> as a universe consists of all the lists of Number’s subtypes. I.e. List<Integer>, List<Double>, etc, are subtype of List<? extends Number>. [image:FBFA8A4A-9FC1-4F1E-888E-4A8CFBFC9883-1030-0000074213A9CF59/E0CD5FFA-A27B-4347-B070-5AF7E47B02F6.png] On the contrary, super can be used to make the List contravariant. We can think of List<? super Numbers> as a universe consists of all the list of Number’s supertypes. I.e. List<Number> and List<Object> are both subtype of List<? super Number>. [image:39C7DFD9-DD08-40AD-89A8-33BBCE49B338-1030-0000073B04008575/76334597-82B0-4913-96C6-F3EAE96F076A.png]

https://gist.github.com/90796eead9726b4989f0cd267df32687

The extra flexibility comes with some restrictions on the operation that you can do to the list, depending on whether the lis is covariant or contravariant. These rules are there to enforce type safety of the generics.

Extends, Read-only, Covariance, Producer

The extends bound creates a covariant and read-only list, or a Producer of elements.

Let’s write a function that takes a list of Number, and print the elements. It uses extends to take advantages of covariance (can accept any subtypes of Number), bearing the consequences of the list being read-only. https://gist.github.com/1e77a7ebf06314b7070054c94467d433

Advantage

You are allowed to get Number out of the list, because everything that extends Number must be a Number.

Restriction

You are not allowed to add anything to the list, because it could be a list of Integer, Double, etc. It is unsafe to make any assumption about the list. You don’t want to add a BigDecimal into a List of Float.

Super, Write-Only, Contravariance, Consumer

The super bound creates a contravariant and write-only list, or a Consumer of elements.

Why does a list needs to be contravariant in order for us to write something into it? As we have seen above, you can’t add anything to a List<? extends Number> because doing so might violate type safety. Then what will be the criteria for us to write any Number to a list? Well, the list has to be general enough to accept any type of Number. Obviously, only Number and its supertype Object is capable of doing so, which is exactly what List<? super Number is.

Let’s write a function that adds any Number into a list. It uses super to take advantages of contravariance (can add any subtypes of Number), bearing the consequences of the list being write-only. https://gist.github.com/68d8b8fcbb2fae561a589cc8716a95f2

Advantage

You are allowed to add any subtypes of Number into the list, because everything that is a supertype of Number, will be able to accept a Number.

Restrictions

You are not allowed to get Number out of the list, because it could be a List<Object>.

Take a look at the implementation of java.util.Collections#addAll method.

PECS (Producer Extends, Consumer Super)

PECS is rule of thumb for applying bounded wildcard to generic Collections. As we’ve seen above, if you need a read-only list (producer), use extends. If you need a write-only list (consumer), use super.

Read and Write

What if we want the function to be able to read and write to list?

Invariance

Invariance is the strictest variance, so an invariant list (without extends and super) will be able to support both read and write operation. This is because the only type that you can read or write is the declared type parameter. E.g. With List, you can only read and write String. https://gist.github.com/f0ae479c058e6e6da5d2b33a4133babc

Covariant Source and Contravariant Destination

You could pass read-only source list and a write-only destination list into the function. This is a simplified extract from Java Collections’ copy method. https://gist.github.com/1e323e19ca25a313128aad06db2cff03

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment