Covariance and Contravariance in Java
Given that Cat is
a subclass of Animal,
should List<Cat>
also be a subclass of List<Animal>
?
What do you think? Should a List<Cat>
be a List<Animal>
? What is Function<? super T, ? extends U>
in Java? Understanding the concept of variance is the key to answering these two seemingly separate questions.
Covariance
Wouldn’t it be great if wherever a List<Animal>
is accepted, you can pass in a List<Cat>
too? But would there be any issue if the compiler allowed that?
Let’s investigate that by first defining the type List<T>
as follows, just so our discussion can stay simple:
Now let’s create a function foo
which takes a List<Animal>
as input. In the body of the function, we get the first element from the list, and call some of its methods:
Let’s see what would happen if we try to pass in a List<Cat>
where a List<Animal>
is accepted:
Line 2 of foo
in Listing 2 accesses the first element of the input list, which turns out to be a Cat
. But that’s no problem, because foo
only accesses methods like getName()
(in line 3) which are defined on Animal.
Any instance of Cat
should also have that method, since Cat
is a subtype of Animal
. So it works out.
In fact, we say the type parameter T
is covariant in the generic type M<T>
if the following are both true:
A
is a subtype ofB
.M<A>
is a subtype ofM<B>
.
In languages like Scala or C#, you can mark the type parameters in a generic class as covariant with a special symbol or a keyword:
But in Java, there is no equivalent of these markers. In fact, with the List<T>
that we defined earlier, Java won’t allow a List<Cat>
to be passed to a List<Animal>
.
So how can we achieve covariance in Java? There’s no way to do that at the definition of the List<T>
. Turns out, you can enforce covariance when the list type is used. Let’s revisit foo
and rewrite it like this:
Notice the use of the wildcard (the question mark). Now we can pass a List<Cat>
or a List
of any subtype of Animal
.
Contravariance
You might ask if there’s exisits a reversed case where, if A
is a subtype of B
, M<B>
is a subtype of M<A>
. There is! And that’s called contravariance.
Consider a comparator type:
And a function bar
which takes a Comparator<Cat>
as input:
Now let’s attempt to pass a Comparator<Animal>
to bar
:
Will this cause issues? No. Line 4 of Listing 7 passes Cat
instances to isGreaterThanOrEqualTo
, which is fine, because the method expects Animal
instances.
In fact, we say the type parameter T
is contravariant in the generic type M<T>
if the following are both true:
A
is a subtype ofB
.M<B>
is a subtype ofM<A>
.
Similarly, in Java, if we need to allow contravariance, we need to declare that where Comparator
is used:
What Really Is The Difference?
Notice that the T
in List<T>
only appears as a return value (see T get(int index)
). Also notice that the T
in Comparator<T>
only appears as an input to a function (see boolean isGreaterThanOrEqualTo(T t1, T t2)
).
So generally, if your type parameter only appears as a return value in the methods in the generic type, it is most likely covariant. Similarly, if it only appears as a input type of the methods, it is most likely contravariant.
The Interesting Case of The Function Interface
Now let’s come back to the example of the Function
interface gave at the beginning of this article. Here’s more context:
Why is T
defined as contravariant and U
as covariant? Let’s look at the Function
interface:
T
only appears as an input type, and U
only appears as an output type, making T
covariant and U
variant. This means, given that Cat
is a subtype of Animal
, and that Car
is a subtype of Vehicle
, we will have that Function<Animal, Car>
is a subtype of Function<Cat, Vehicle>
. That is to say, whenever a Function<Cat, Vehicle>
is expected, you may pass a Function<Animal, Car>
to it.
Conclusion
Next time you see ? extends T
or ? super U
, don’t be surprised, as the author of the code is just trying to get around the limitation of Java’s type system to give you maximum satisfaction of the Liskov substitution principal.