Understanding Contravariance — The Java Wildcard

A second guide to exploring this fantastic element

Vishal Ratna
Better Programming

--

Photo by Nicolas Brulois on Unsplash

Contravariance is tricky, so go through the article slowly! Do not rush. If you’re in a hurry, add it to the reading list and return to it later.

This is the continuation of the article in the series. It would be useful if you go through the previous article first. You will have the historical context of things discussed in this article.

As we learnt in part A of this series:

  • If String is the child of Object (call it base-relation), then how can we use contravariance to build a relation where HT<Object> is the child of HT<String>? Where HT is a holder data type like List, Map, etc.
  • Contravariance inverts the base relationship, according to contravariance, If String is the child of Object, then HT<Object> will be the child of HT<String>. This means the parent references could be allowed to store the child references.

This does not come to us out of the box. We need to use the Java wildcards.

We will use a similar example in the previous post to understand more. Consider a class hierarchy like the following:

great-grandfather, grandfather, father, child, grandchild, great-grandchild
the class hierarchy of child

There is a method that accepts List<Child> as the argument, and now we want to accept a List<Father>, List<Grandfather>, List<GreatGrandFather> ..up to List<Object>. But this is opposite to what we have learnt in covariance. The situation is depicted below:

function accepting class ancestors

This is done by declaring the function like this:

void accept(List<? super Child> items)

Once this is done, the function will start accepting all the holder types of itself and its ancestors, making it look like the HT<Parent> is the child of HT<Child>. Java child references can be assigned to the parent references, not vice versa. You played your wildcard! So the name. See the code snippet below:

Superclasses are getting assigned to sub-class references using contravariance

Now, let’s analyse the accept() method. We know that the items could contain any of the child’s ancestors, including Object. So, can we guarantee what reference can be read from this contravariant list. No! The only guarantee that we have is we can extract an Object reference safely.

Restrictions of Contravariance

We are not allowed to read anything apart from Object, from the contravariant holder object.

Sample showing only reference to Object can be extracted confidently.

2. The second manifestation is opposite to what we have seen in the covariant lists in the previous post. We will be able to add an object of Child or any subclass of Child in the list `items,` but nothing above it. Why?

Suppose the list passed to the method is a List<Father>. If we allow classes above Child into the list, then developers may pollute the list accidentally — adding GrandFather to the List<Father> will be wrong and could lead to ClassCastException. But, Child and subclasses of Child will always make sure the list is consistent.

Sample showing contravariant list can accept any subclass of the child but nothing above it.

A question comes into mind: why would we ever need this relationship that contravariance facilitates?

Suppose you have written a utility function that copies the contents of List<Child> into another.

void copy(List<? super Child> dest, List<? extends Child> source)

We understand that the source will accept any list with a Child subclass. That is simple to understand, as covariance makes the code more generic to work for multiple subclasses of Child. But how does List<? super Child> help?

In the dest, we can pass List<Father>, List<GrandFather>, etc., when you are passing these destination lists to copy(), you would also have code to use this list too, right? You will realise that the usage contract of the lists won’t break with contravariance, and still, the code is kept as generic as possible.

See the following code:

Contravariance allowed your copy() method to be used by List<> of Child’s super class also without breaking their contracts otherwise you could have to write a separate method of father, grandfather etc.

Contravariance beyond collections

Let us consider the same example of Box<T>:

Now, we know the relationship between GrandFather, Father, and Child. Using contravariance, we can invert the relation of Box<> classes holding GrandFather, Father, and Child.

See that there is no out-of-the-box relationship between the Box instances of each of the instances.

No relationship b/w box instances

But, after applying contravariance, we will be able to assign Box<Parent> to Box<Child>.

Contravariant behavior of Box class

Just like in collection, once we are accessing the contravariant reference of Box, we will be allowed to add any implementation of child in the setItem() method, but nothing above Child. See below. Child and Grandchild were allowed, but not Father.

We also observe that from the contravariant reference of Box, we cannot extract any reference other than Object. The same way it behaved in cases of collections. We were not allowed to read anything apart from Object.

Contravariant list allows reading object reference only

What Did We Learn

  1. Contravariance can be used to invert the parent-child relationship between the containing objects.
  2. It can be used to make a holder type write-only and block reading any reference apart from Object.
  3. The contravariant list will allow any child of the bounding type to be added. For example, Box<? super Child> will allow Child and its subtypes but not anything above Child in the hierarchy.

Contravariance in Java vs Covariance in Kotlin

In Java, we saw that contravariance can be obtained by using <? super SomeClass>. But, this can be done only outside the holder class. As in the above example of Box, the new Box reference we created outside the Box class was declared contravariant and not the actual Box class.

This is called call-site variance. As the variance is defined at the usage site. So, it is also called use-site variance.

In Kotlin, contrary to what we have seen, we can declare a class to be contravariant at the time of writing the holder class itself by using the in operator.

We see that declaring a class in starts showing an error when we write a method that returns T as a result. In other words, declaring T as in will not allow you to write any method that accepts returns T. T can only be an argument to a method, aka, T can only come inside. So the in.

We get the below behaviours for free at the call-site in Kotlin. We had to create contravariant references for the same in Java.

  1. We are not allowed to put any object that is above Child in the hierarchy. The following example shows how it fails in the case of passing the Grandfather object, and works with the Child and Grandchild objects.

2. KtBox<Child> references allowed to store KtBox<Parent>

Kotlin covariance can be defined at the time of creating the holder class. So, it is also called “declaration site contravariance.”

Examples of when to use contravariance, aka <? super XXXX>

  1. It is opposite to covariant APIs. If you want to build interceptor APIs where you do not want to read anything, add some items. For example, once the user submits work to your framework, you want to add a couple of default work items to the list without reading anything.

2. For other holder types like Box<T>, we could use contravariance to prohibit reading objects of type T from it.

Parting Words

Now you have learnt the fundamentals of covariance and contravariance, we can start learning how these can be used to build advanced API surfaces, such as when that situation arrives when you need a relationship where you should be able to assign parent references to child and vice versa.

--

--

Senior Android Engineer @ Microsoft. Fascinated by the scale and performance-based software design. A seeker, a learner. Loves cracking algorithmic problems.