Better Programming

Advice for programmers.

Follow publication

A Deep Dive Into Java Wildcards — Covariance

An exploration of one of the more difficult topics in Java

Vishal Ratna
Better Programming
Published in
8 min readSep 15, 2022

--

Photo by Sixteen Miles Out on Unsplash

When I encountered wildcards, I was extremely confused, especially when it came along with the <T>s,<U>s,<V>s. There was confusion on when we should be using <T extends Number> vs <? extends Number>. I am sure many of you may also be confused.

Today, we will try to understand the <? extends Bla>. I will not talk about standard things like PECS (Producer Extends, Consumer Super), etc. I have read about it, but it is not just about memorising things. I believe we should always touch the core.

Before we dig deep, let’s try to understand the back story. This will make things easier. So, hop onto the ride. It might be a long one.

Read this slowly. I have provided lots of code and details for clear visualisation.

Covariance and Contravariance — Important Concepts!

Covariance — We know that String is a child of Object, so according to the rules of Java, we can assign a child object reference to a parent object. Something like the following:

String s = "Wildcards";
Object o = s;

So, according to covariance rules, this is possible:

String[] sArray = { "Wildcards" };
Object[] o = sArray; // Valid in Java.

We call String[] and Object[] a holder type (HT — that holds some objects) that applies to held objects like String and Object, respectively. That HT can be List<>, Set<>, Box<> — anything that can hold objects.

So, according to covariance, if Object is the parent of String, then HT<Object> will also be parent of HT<String>.

List<String> s = new ArrayList<>();
List<Object> o = new ArrayList<>();
// This will not compile! But this is what covariance is.
o = s; // We could have done this if lists were covariant, but List is not covariant in its plain form.

Arrays are covariant in Java, which means the following code is valid.

String[] sArray = { "Wildcards" };
Number[] nArray = { 2, 3 };
Object[] o = sArray; // Object is parent of String
o = nArray; // Object is parent of Number too!

Making arrays covariant was a Java design decision. They could have chosen a different path. But, making them covariant allowed a lot of polymorphic behavior. People could write generic code by storing business objects inside Object[]. But it introduced bugs that could only be detected at runtime. Consider the following situation:

Number[] nArray = { 2, 3 };
Object[] o = nArray;
o[0] = "s"; // This is valid in Java, but will crash with ArrayStoreException.

In big enterprise applications and libraries, this kind of error is very much possible and could crash and cause lots of damage.

Era of collections

When collections were introduced, they were not written the way they are now. They did not have the <T> information with them. Still, you can write code without type information. The IDE will make your statements yellow, and the compiler will show warnings. Even today, when you provide type information, after verifying everything compiler erases the type information at the time of compilation.

This is called “type erasure.” Consider the example below:

IDE warnings

You can see that we can add any element to the list, and the IDE is bleeding. This behavior was retained so the legacy code does not break and things are backward compatible. In the Java byte code, there is no type information.

So, Java added type information in collections from Java 5. And since then, the compiler has tried to catch illegal assignments. And it is pretty successful, or is it?

Now, if we try to do the same thing after adding type, voila! It catches.

Compiler warning on illegal assignment

Let’s try to get our hands dirty and try something real. Assume you are building a framework that has a scheduler and task. For that, you have a base task and multiple implementations of it.

startJob(task) can accept multiple types of work. Now, the requirements change, and we need to submit a list of work. Easy stuff! We make the changes again. And darn, this happens! “List<RxWork> is not allowed.”

Collections are not covariant directly without wildcards.

This happens because of the following:

Even if RxWork is a child of BaseWork, List<RxWork> is not a child of List<BaseWork>.”

But hey! Why not? Why did the Java engineers keep us from doing this?

Try to understand it this way: Suppose they had allowed us to pass List<RxWork> also. Then, while extracting the work object, someone could have used the RxWork reference to extract the work item like below. And, if the item were actually a BaseWork, then the reference would have been assigned to the child reference — which would have caused runtime crashes.

void startJob(List<BaseWork> incomingWork) {
// validate the work and submit.
for (RxWork b : incomingWork) { // Will crash as BaseWork cast to RxWork will give ClassCastException in runtime. Same problem as arrays.
Scheduler.submit(b);
}
}

To avoid the same pitfall that made arrays risky, this is not allowed in collections.

But, this is a valid technical use case, and Java engineers knew this. To do what you want, you must declare your List as covariant. And a safe covariance behavior can only be allowed if the compiler guarantees that no one will be allowed to extract anything apart from BaseWork. In that case, if the list implements BaseWork, we are always safe!

We can make List<BaseWork> covariant by doing the following:

List<? extends BaseWork>

In the above code, see how we can pass a list of any BaseWork implementation. And see how extracting RxWork shows an error in the covariant list.

But we engineers are smart, and what if modify the list inside the startJob() method. To outsmart that smartness, that is blocked too. Once you access the reference of a covariant list, you cannot add anything to it. Here, incomingWork is covariant.

This makes sure you never end up with ClassCastException! in the runtime. The compiler makes sure that if we are accessing the covariant list of BaseWork , then the item is at least BaseWork, so it is allowed. You get the best of both worlds. You can write generic code by creating a relation, List<RxWork> is subtype of List<BaseWork>, and you do not end up with runtime errors.

This is how you play your wildcard! And so it’s aptly named. This is also called applying the “upper bound.” As it will hold any subtype of the class coming after extends keyword.

Covariance outside collections

Is covariance just relevant to Collections? No.

To understand how we leverage wildcards in building nice APIs. Let’s assume there is a class hierarchy. “Child” is a subtype of “Father,” and “Father” is a subtype of “GrandFather.” See the hierarchy below.

And there is a class called Box that can hold objects.

Now, we know the relationship between grandFather, Father, and Child. Let’s see if we can build a similar relation between the Box holding these objects.

We can see that grandFather reference can hold Father. But Box<GrandFather> can’t hold Box<Father>.

From what we’ve learned, we know we have to make the Box reference covariant. Let’s try to do it and see what happens.

Now, Box<Father> can be assigned to Box<? extends Grandfather>.

The covariant list will manifest the two behaviours:

  1. We can only extract the reference to the upper bounded class’s object(grandFather in this case). Let’s experiment. We see below that we can easily extract grandFather but extracting Father fails.

2. We cannot modify the content of the Box class same as we were not able to do in the List<>. The compiler will block us from using the setItem(T item) method. Strange, right?

We can make the container class readonly. We played our wildcard again!

What We Learned

  1. Covariance can be used to establish the same parent-child relationship between holder types(HT) as there is between the containing objects — where there is no relationship provided by Java out of the box.
  2. Can be used to make a holder type readonly (Reading only the upper bound type).
  3. Restrict the HT to return only the “upper bound” class reference.

Covariance in Java vs Covariance in Kotlin

In Java, we saw that the covariance could be obtained by using <? extends SomeClass>. But this can be done only outside the holder class. As in the above example of Box, the new Box reference that we created outside the Box class was declared covariant 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, one step ahead of what we have seen in Java, we can declare a class to be covariant when writing the holder class itself by using the out operator.

Usage of out operator.

We see that declaring a class out starts showing an error when we write a method that accepts T as an argument. In other words, declaring T as out will not allow you to write any method that accepts T as an argument. T can only be a return type, hence the name out.

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

  1. Only the upper bound class can be extracted from the KtBox object.

2. KtBox<Father> can be assigned to KtBox<Grandfather> without any additional work we did in Java by creating covariant references.

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

Examples of When To Use Covariance, Aka <? Extends XXXX>

  1. While building an API that accepts some holder classes from the user and operates on that. For example, create a startWork(List<? extends BaseWork works) method that accepts a list of work. And there can be multiple implementations of work objects, such as RxWork, CoroutineWork, ThreadWork, etc.
  2. We also do not want to modify the user request to it will make the references inside your framework methods readonly.
  3. When you want to build a holder class like Box<>, but you want it to be readonly.

Parting Words

This was a long article, but I hope you get the gist. There is another kind of variance called contravariance, which is exactly the opposite of that. We will discuss it in detail in the next article.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Vishal Ratna
Vishal Ratna

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

Responses (2)

Write a response