Covariance and Contravariance in Java

Given that Cat is a subclass of Animal, should List<Cat> also be a subclass of List<Animal>?

Yuhuan Jiang
3 min readFeb 8, 2019

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:

Listing 1. A simple definition of the type List<T>.

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:

Listing 2. A sample function which accepts and consumes a List<Animal>.

Let’s see what would happen if we try to pass in a List<Cat> where a List<Animal> is accepted:

Listing 3. Passing a List<Cat> to a function expecting a List<Animal>.

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 of B.
  • M<A> is a subtype of M<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:

Listing 4. Marking a type parameter as covariant with a plus sign.

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:

Listing 5. Declaring covariance in Java.

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:

Listing 6. A simple definition of Comparator<T>.

And a function bar which takes a Comparator<Cat> as input:

Listing 7. A function bar which excepts a Comparator<Cat>.

Now let’s attempt to pass a Comparator<Animal> to bar:

Listing 8. Passing 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 of B.
  • M<B> is a subtype of M<A>.

Similarly, in Java, if we need to allow contravariance, we need to declare that where Comparator is used:

Listing 9. Declaring contravariance in Java.

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:

Listing 10. In Function<T, U>, T as contravariant and U as variant.

Why is T defined as contravariant and U as covariant? Let’s look at the Function interface:

Listing 11. T is an input while U is an output in Function<T, U>.

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.

--

--