Understanding Contravariance — The Java Wildcard
A second guide to exploring this fantastic element

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 ofHT<String>
? Where HT is a holder data type likeList
,Map
, etc. - Contravariance inverts the base relationship, according to contravariance, If String is the child of
Object
, thenHT<Object>
will be the child ofHT<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:

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:

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:

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.

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.

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.

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

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
.

What Did We Learn
- Contravariance can be used to invert the parent-child relationship between the containing objects.
- It can be used to make a holder type write-only and block reading any reference apart from
Object
. - The contravariant list will allow any child of the bounding type to be added. For example,
Box<? super Child>
will allowChild
and its subtypes but not anything aboveChild
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.
- 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 theGrandfather
object, and works with theChild
andGrandchild
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>
- 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.