Generics PECS - [OOP & Java #11]

tlylt

Liu Yongliang

Posted on December 10, 2020

Generics PECS - [OOP & Java #11]

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?
Enter fullscreen mode Exit fullscreen mode

Assignment

Assignment statements honor the invariance relationship of generics

// ERROR
List<Number> list1 = new ArrayList<Double>();
// SAFE
List<Integer> list2 = new ArrayList<Integer>();
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode
Contravariance
  • Lower bound wildcard parameterized type (<? super Integer>)
// SAFE
List<? super Integer> list1 = new ArrayList<Number>();
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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) 
Enter fullscreen mode Exit fullscreen mode

Let's look at case 2: ? super

List<? super Integer> list2 = new ArrayList<Integer>();
Enter fullscreen mode Exit fullscreen mode
  • 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);
Enter fullscreen mode Exit fullscreen mode
  • 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 ? with Integer,
  • 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, suppose A 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 into ArrayList<Comparable<Integer>>
  • and similarly Comparable<Integer> should not go into ArrayList<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>>();
Enter fullscreen mode Exit fullscreen mode

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 😏

image

References:

Generics FAQ here and FAQ here

💖 💪 🙅 🚩
tlylt
Liu Yongliang

Posted on December 10, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Command Pattern
design Command Pattern

November 28, 2024

Abstract Factory Pattern
design Abstract Factory Pattern

November 17, 2024

Singleton Pattern
computerscience Singleton Pattern

November 22, 2024

Simple Factory
computerscience Simple Factory

November 10, 2024