by Fabian Terh
An introduction to generic types in Java: covariance and contravariance
Java is a statically typed language, which means you must first declare a variable and its type before using it.
int myInteger = 42;
Enter generic types.
Definition: “A generic type is a generic class or interface that is parameterized over types.”
Essentially, generic types allow you to write a general, generic class (or method) that works with different types, allowing for code re-use.
Rather than specifying
obj to be of an
int type, or a
String type, or any other type, you define the
Box class to accept a type parameter
<;T>. Then, you ca
n use T to represent that generic type in any part within your class.
Now, enter covariance and contravariance.
Covariance and contravariance
Variance refers to how subtyping between more complex types relates to subtyping between their components (source).
An easy-to-remember (and extremely informal) definition of covariance and contravariance is:
- Covariance: accept subtypes
- Contravariance: accept supertypes
In Java, arrays are covariant, which has 2 implications.
Firstly, an array of type
T may contain elements of type
T and its subtypes.
Number nums = new Number;nums = new Integer(1); // Oknums = new Double(2.0); // Ok
Secondly, an array of type
S is a subtype of
S is a subtype of
Integer intArr = new Integer;Number numArr = intArr; // Ok
However, it’s important to remember that: (1)
numArr is a reference of reference type
Number to the “actual object”
intArr of “actual type”
Therefore, the following line will compile just fine, but will produce a runtime
ArrayStoreException (because of heap pollution):
numArr = 1.23; // Not ok
It produces a runtime exception, because Java knows at runtime that the “actual object”
intArr is actually an array of
With generic types, Java has no way of knowing at runtime the type information of the type parameters, due to type erasure. Therefore, it cannot protect against heap pollution at runtime.
As such, generics are invariant.
ArrayList<Integer> intArrList = new ArrayList<>();ArrayList<Number> numArrList = intArrList; // Not okArrayList<Integer> anotherIntArrList = intArrList; // Ok
The type parameters must match exactly, to protect against heap pollution.
But enter wildcards.
Wildcards, covariance, and contravariance
With wildcards, it’s possible for generics to support covariance and contravariance.
Tweaking the previous example, we get this, which works!
ArrayList<Integer> intArrList = new ArrayList<>();ArrayList<? super Integer> numArrList = intArrList; // Ok
The question mark “?” refers to a wildcard which represents an unknown type. It can be lower-bounded, which restricts the unknown type to be a specific type or its supertype.
Therefore, in line 2,
? super Integer translates to “any type that is an Integer type or its supertype”.
You could also upper-bound the wildcard, which restricts the unknown type to be a specific type or its subtype, by using
? extends Integer.
Read-only and write-only
Covariance and contravariance produce some interesting outcomes. Covariant types are read-only, while contravariant types are write-only.
Remember that covariant types accept subtypes, so
ArrayList<? extends Number> can contain any object that is either
of a Number type or its subtype.
In this example, line 9 works, because we can be certain that whatever we get from the ArrayList can be upcasted to a
Number type (because if it extends
Number, by definition, it is a
nums.add() doesn’t work, because we cannot be sure of the “actual type” of the object. All we know is that it must be a
Number or its subtypes (e.g. Integer, Double, Long, etc.).
With contravariance, the converse is true.
Line 9 works, because we can be certain that whatever the “actual type” of the object is, it must be
Integer or its supertype, and thus accept an
But line 10 doesn’t work, because we cannot be sure that we will get an
Integer. For instance,
nums could be referencing an ArrayList of
Therefore, since covariant types are read-only and contravariant types are write-only (loosely speaking), we can derive the following rule of thumb: “Producer extends, consumer super”.
A producer-like object that produces objects of type
T can be of type parameter
<? extends T>, while a consumer-like object that consumes objects of
meter <? super T>.