In Part 4, I introduced you with the concept of invariance. In this article, I will cover covariance.
I will use the below type hierarchies for our discussion.
Get Started
To start, I will introduce you with the code for the above type hierarchies as given below.
public class EaterA {
protected String myUUID;
public EaterA() {
UUID uuid = UUID.randomUUID();
this.myUUID = "A--" + uuid.toString();
}
public String getMyUUID() {
return this.myUUID;
}
public void eat() {
System.out.println("I am eating apple.");
}
public void eatPizza() {
System.out.println("I am eating Italian pizza.");
}
}
public class EaterB extends EaterA {
public EaterB() {
UUID uuid = UUID.randomUUID();
this.myUUID = "B--" + uuid.toString();
}
@Override
public String getMyUUID() {
return this.myUUID;
}
@Override
public void eat() {
System.out.println("I am eating banana.");
}
public void eatChocolate() {
System.out.println("I am eating dark chocolate.");
}
}
public class EaterC extends EaterB {
public EaterC() {
UUID uuid = UUID.randomUUID();
this.myUUID = "C--" + uuid.toString();
}
@Override
public String getMyUUID() {
return this.myUUID;
}
@Override
public void eat() {
System.out.println("I am eating Cowberry.");
}
@Override
public void eatChocolate() {
System.out.println("I am eating milk chocolate.");
}
}
public class EaterG extends EaterB {
public EaterG() {
UUID uuid = UUID.randomUUID();
this.myUUID = "G--" + uuid.toString();
}
@Override
public String getMyUUID() {
return this.myUUID;
}
@Override
public void eat() {
System.out.println("I am eating grapefruits.");
}
}
public interface Eater {
public void eat();
public String getMyUUID();
}
public class EaterD implements Eater {
private String myUUID;
public EaterD() {
UUID uuid = UUID.randomUUID();
this.myUUID = "D--" + uuid.toString();
}
@Override
public String getMyUUID() {
return this.myUUID;
}
@Override
public void eat() {
System.out.println("I am eating dragon fruit.");
}
public void eatBurger() {
System.out.println("I am eating veggie burger.");
}
}
public class EaterE implements Eater {
protected String myUUID;
public EaterE() {
UUID uuid = UUID.randomUUID();
this.myUUID = "E--" + uuid.toString();
}
@Override
public String getMyUUID() {
return this.myUUID;
}
@Override
public void eat() {
System.out.println("I am eating elephant apple fruit.");
}
public void eatBurger() {
System.out.println("I am eating veggie burger.");
}
}
public class EaterF extends EaterE {
public EaterF() {
UUID uuid = UUID.randomUUID();
this.myUUID = "F--" + uuid.toString();
}
@Override
public String getMyUUID() {
return this.myUUID;
}
public void eatSushi() {
System.out.println("I am eating nigiri sushi.");
}
}
I will also introduce with the below generic class.
/**
* MyContainer contains 3 elements of type T.
*
* @param <T>
*/
class MyContainer<T> {
// capacity denotes the total number of elements this container can hold
private int capacity;
// currentIndex holds the current position in dataArray
private int currentIndex;
// dataArray holds the three elements of MyContainer
// dataArray can hold any Java objects since Object
// is root class of all Java classes
private Object[] dataArray;
public MyContainer() {
this.capacity = 3;
this.dataArray = new Object[this.capacity];
this.currentIndex = -1;
}
public int capacity() {
return this.capacity;
}
public int size() {
return this.currentIndex + 1;
}
@SuppressWarnings("unchecked")
public T get(int index) {
if (index > this.currentIndex) {
return null;
} else {
return (T) dataArray[index];
}
}
public boolean add(int index, T t) {
if (index < 0 || index > (this.capacity - 1)) {
return false;
}
//T will be implicitly upcasted into Object by the compiler
this.dataArray[index] = t;
this.currentIndex = reCalulateIndex();
return true;
}
private int reCalulateIndex() {
int count = -1;
for(int i= 0; i < this.capacity; i++) {
if (dataArray[i] != null) {
count = count + 1;
}
}
return count;
}
public void clear() {
for (int i = 0; i < this.currentIndex; i++) {
this.dataArray[i] = null;
}
this.currentIndex = -1;
}
}
For our convenience, I will summarize the public methods of the generic class MyContainer<T> as follows:
Now we will have a quick look at usage of MyContainer<T> generic class. Also, we will try to visualize the same through memory and object diagram as follows.
We will have a look at another usage of MyContainer<T> as follows:
One more example of usage of MyContainer<T>…
Since EaterB and EaterC are subtypes of EaterA, we can add objects of types EaterB and EaterC in refEaContainer2 (See Figure C). From this observation, we may think that MyContainer<EaterB> is a subtype of MyContainer<EaterA> and MyContainer<EaterC> is a subtype of MyContainer<EaterA>.
But it is not. Therefore, we will get error “cannot convert from MyContainer<EaterB> to MyContainer<EaterA>” if we try to assign refEbContainer to refEaContainer (See Figure D).
Now question is: Can’t we build a subtyping relationship between two parameterized types of a generic class?
Yes, we can. For that purpose, we need the help of variance. Variance helps us build a subtyping relationship between parameterized types.
According to Cambridge Dictionary (online version), one of the meanings of variance is as follows:
According to Merriam-Webster Dictionary (online version), one of the meanings of variance is as follows:
Basically, when we talk about variance, we basically mean the state of being variant. When something is variant, it allows variation of a particular thing. In current context, we apply the notion of variance at the type parameter level. Moreover, variance follows few rules. Rules will depend on the type of the variance. The two most important types of variance are covariance and contravariance. In this article, we will briefly cover the core idea of covariance.
Covariance
To take advantage of covariance, we use upper bounded wildcards as follows.
MyContainer<? extends EaterA> refEaCoContainer;
MyContainer<EaterB> refEbContainer = new MyContainer<EaterB>();
refEaCoContainer = refEbContainer;
Here, MyContainer<? extends EaterA> is a covariant instantiation of MyContainer. The expression “? extends EaterA“ is an upper bounded wildcard where EaterA is the upper bound. Moreover, this expression represents a type that is either EaterA or any subtype of EaterA. In turn, parameterized type MyContainer<? extends EaterA> works as a supertype of all parameterized types MyContainer<S> or MyContainer<? extends S> where S is a subtype of EaterA.
One important point to note here is that we didn’t tell anything about variance (at the type parameter level) when we declared the generic type MyContainer<T>. Instead, we define the variance through variance annotation(? extends) during the usage of a generic class (i.e., during instantiation) in Java. We call it use-site variance.
What is going on when we are using covariance?
To answer this question, we will consider a simple example for better understanding.
The MyGeneric<T> declaration has two public methods. Any invariant instantiations of MyGeneric<T> will be able to access those two public methods. To understand this, consider the below code snippet and Figure G. In the Figure G, you think about the interface conceptually.
MyGeneric<A> refMga = new MyGeneric<>();
refMga.set(new B());
refMga.get();
Now consider the below code snippet.
MyGeneric<B> refMgb = new MyGeneric<>();
refMgb.set(new B());
MyGeneric<? extends A> refMga2 = refMgb;
refMga2.get();
The covariant instantiation put restrictions on accessibility of members of the corresponding generic class. The method call refMga2.set(…) will give compilation error saying “the method set(capture#2-of ? extends A) in the type MyGeneric<capture#2-of ? extends A> is not applicable for the arguments (B)“. It means compiler is expecting some other subtype of A, but we are providing type B. Basically, we won’t be able to add anything to a covariant structure.
Now consider the below code snippet:
MyGeneric<B> refMgb = new MyGeneric<>();
refMgb.set(new B());
MyGeneric<? extends A> refMga2 = refMgb;
A b = refMga2.get();
MyGeneric<C> refMgc = new MyGeneric<>();
refMgc.set(new C());
refMga2 = refMgb;
A c = refMga2.get();
In the above code, the reference variable refMga2 is sometimes attached with the “MyContainer of B“; other times, it is attached with the “MyContainer of C” and so on. Basically, compiler cannot ensure the exact type of the data residing inside the MyContainer through the reference variable refMga2. Therefore, compiler doesn’t allow us to add safely in this structure. But compiler can ensure one thing is that the type of data inside this structure (MyGeneric<? extends A>) is subtype of A. Therefore, we can get data from this structure through some public method like get and assign the same to any reference variable of type A (look at line numbers 5 and 11) because type of data inside this structure (MyGeneric<? extends A>) is a subtype of A. So, subtype polymorphism does the magic through substitution principle.
Conclusion
We use upper bounded wildcards (? extends SomeUpperBound) for covariance. Remember, we only get data out of a covariant structure. We cannot put anything in it.
That’s all for covariance for the time being.
In next article, I will discuss about contravariance.
I have shared knowledge based on my understanding and experiences. If you find any significant errors or want to give me some feedback, feel free to contact me at maliksanjoykumar[@]gmail.com.
Sanjoy Kumar Malik is an experienced software architect and technologist. He is passionate about Cloud Computing, Software Architecture, and System Design. Apart from technology and software, he is an avid LinkedIn networker. You can join his 5.5+ lacs supporters on LinkedIn.