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.
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
- You can assign an
Integer[]
to aNumber[]
, just like you canNumber number = new Integer(1)
. - At runtime, Java knows that
numArray
is anInteger[]
. - You can add a
Double
tonumArray
with no compilation error, but at runtime, you get ArrayStoreException.
- Arrays are covariant.
- Array is a Reifiable Type (link to previous post). After compilation, numArray is reified to
Integer[]
. - At compile-time,
numArray
is just aNumber[]
, that’s why you can add aDouble
(subtype ofNumber
) 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.
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[].
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.
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.
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.
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
You are allowed to get Number out of the list, because everything that extends Number must be a Number.
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.
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
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.
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 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
.
What if we want the function to be able to read and write to list?
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
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