Generics PECS - [OOP & Java #11]
Liu Yongliang
Posted on December 10, 2020
We will use the following code for ease of reference and discussion.
// assignment
List<? extends Number> list1 = new ArrayList<Double>();
List<? super Integer> list2 = new ArrayList<Integer>();
// taking, suppose not empty
Number num1 = list1.get(0); // why works fine?
Integer num2 = list2.get(0); // why compiler-error?
// putting
list1.add(45); // why compiler-error?
list2.add(45); // why works fine?
Assignment
Assignment statements honor the invariance relationship of generics
// ERROR
List<Number> list1 = new ArrayList<Double>();
// SAFE
List<Integer> list2 = new ArrayList<Integer>();
However, bounded wildcards provide two other kinds of relationship:
Covariance
- Upper bound wildcard parameterized type (
<? extends Number>
)
// SAFE
List<? extends Number> list1 = new ArrayList<Double>();
Contravariance
- Lower bound wildcard parameterized type (
<? super Integer>
)
// SAFE
List<? super Integer> list1 = new ArrayList<Number>();
Find out more from previous articles about generics if the above is not yet intuitive.
Accessing elements
So we know what can be assigned from the right-hand side to the left-hand side, now we can focus on what we can do with the product of that assignment.
It's important to know that what is accessible from the variables like list1
above depends on the information retained in the wildcard bound.
Let's look at case 1: ? extends
List<? extends Number> list1 = new ArrayList<Double>();
- Take
list1
has an unknown type of an upper bound Number
. Following the fact of upper bounded wildcard that
A returned object of "unknown" type is known to be compatible with the upper bound.
We know that we can retrieve objects of the upper bound type, Number
in this case, from the list.
// SAFE
List<? extends Number> list1 = new ArrayList<Double>();
Number num = list1.get(0) // suppose list1 is not empty
When we get to take things out of something, we call it a producer. Thus, we have the first half of PECS => Producer Extends.
- Put
However, because we cannot tell what is the unknown type that is inside list1
(could be Integer
, could be Double
), we cannot put anything in, with the only exception being null
.
List<? extends Number> list1 = new ArrayList<Double>();
// ERROR
list1.add(1);
// OK, but not useful
list1.add(null)
Let's look at case 2: ? super
List<? super Integer> list2 = new ArrayList<Integer>();
- Take
list2
has an unknown type of a lower bound Integer
. This information is not enough to determine if Integer
is inside list2
. Thus, the only possible type of object that can be taken out is ... Object
.
List<? super Integer> list2 = new ArrayList<Integer>();
// ERROR
Number num = list2.get(0); // suppose not empty
Integer num = list2.get(0); // suppose not empty
// OK but not useful
Object num = list2.get(0);
- Put
The lower bound does provide us with the information that we can put an Integer
into the list. This is because Integer
is the lower bound,
and hence:
- replacing
?
withInteger
, - you get
List<Integer super *whatever super of Integer*>
- which will always be valid.
<? super Integer>
means we can only assign List<Integer>
or List<Number>
or any supertype of Integer
for assignment
But for putting/consuming stuff into the container, we can use any subtype of the lower bound.
-
list2.add(45);
is fine -
list2.add(new A());
is also fine, supposeA extends Integer
Here is the second half of PECS => Consumer Super.
We cannot pass/consume a Number
into list2
because the compiler does not know whether the List<? super Integer>
refers to List<Number>
or perhaps to a List<Comparable<Integer>>
, in which case
-
Number
should not go intoArrayList<Comparable<Integer>>
- and similarly
Comparable<Integer>
should not go intoArrayList<Number>
To elaborate:
-
Number
is a class that fits the requirement i.e.List<Number super Integer>
-
Comparable<Integer>
is an interface that fits the requirement, i.e.List<Comparable<Integer> super Integer>
. This means the following works for assignment calls, like we said before:
List<? super Integer> l1 = new ArrayList<Number>();
List<? super Integer> l2 = new ArrayList<Comparable<Integer>>();
So, in the two possible cases above, what is the common type that can be added to both lists and still make sense?
The answer is Integer
(and subtype of Integer
).
Conclusion
Truth is I write this so that I can refer to it when I need it someday 😏
References:
Posted on December 10, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.