1 - PECS (Producer Extends, Consumer Super)

USED IN METHOD PARAMETER - AVOID IN RETURN TYPE

  • “Producer Extends” - public void method(List<? extends T> list)
    • cannot add any object to this list
    • can guarantee object of type T to be read from this list
  • “Consumer Super” - public void method(List<? super T> list)
    • can add objects of type T and subtypes of T to this list
    • cannot guarantee what type of object you may read from this list
  • If you need to both read from and write to a list, you need to declare it exactly with no wildcards (e.g. List<T>)

Parameter Produces T Instances?

YES

NO

Parameter Consumes T Instances?

YES

Foo<T>(Invariant in T)

Foo<? super T>
(Contravariant in T)

NO

Foo<? extends T>
(Covariant in T)

Foo<?> Foo<? extends Object> Foo
(Independent of T) Legacy/Raw List example use cases

2 - Examples

Example 1

Note how the source list src (the producing list) uses extends, and the destination list dest (the consuming list) uses super:

public static <T> void copy(List<? extends T> src, List<? super T> dest) {
	for (int i = 0; i < src.size(); i++) {
    	T t = src.get(i);
//		T t = dest.get(i); <-- compiler error
//		T t = (T) dest.get(i); <-- may throw ClassCastException

    	dest.set(i, t);
//		src.set(i, t); <-- compiler error
	}
}
Example 2
public class SortedList<T extends Comparable<? super T>> extends LinkedList<T>

T extends Comparable<? super T> is saying that EITHER:

  • type T has to implement Comparable
  • type T’s superclass has to implement Comparable

So consider java.util.Date. It implements Comparable<Date>. But what about java.sql.Date? It implements Comparable<java.util.Date> as well.

Without the super signature, SortedList would not be able accept the type of java.sql.Date, because it doesn’t implement a Comparable of itself, but rather of a super class of itself

Example 3
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

having map take in the parameter Function<? super T, ? extends R> allows the following:

Function<Number, String> func = String::valueOf;
 
List<Integer> list = List.of(1, 2);
Stream<String> stream = list.stream().map(func);
 
List<Long> list = List.of(1L, 2L);
Stream<String> stream = list.stream().map(func);

3 - Explanation

Imagine having this hierarchy

interface A1{}
interface A2{}
interface A3{}
interface A4{}

interface B1 extends A1{}
interface B2 extends A1,A2{}
interface B3 extends A3,A4{}
interface B4 extends A4{}

interface C1 extends B2{}
interface C2 extends B2,B3{}
interface C3 extends B3{}

interface D1 extends C1,C2{}
interface D2 extends C2{}

interface E1 extends D1{}
interface E2 extends D1{}
interface E3 extends D2{}
interface E4 extends D2{}

3.1 - Extends

By writing
List<? extends C2> list =

you are saying that list will be able to reference an object of type (for example) ArrayList whose generic type is one of the 7 subtypes of C2 (C2 included):

  1. new ArrayList<C2>() - can list.add: C2 D1 D2 E1 E2 E3 E4
  2. new ArrayList<D1>() - can list.add:    D1    E1 E2
  3. new ArrayList<D2>() - can list.add:       D2       E3 E4
  4. new ArrayList<E1>() - can list.add:          E1
  5. new ArrayList<E2>() - can list.add:             E2
  6. new ArrayList<E3>() - can list.add:                E3
  7. new ArrayList<E4>() - can list.add:                   E4

We have a set of “storable” types for each possible case: 7 sets here graphically represented

As you can see, there is not a safe type that is common to every case:

  • you cannot list.add(new C2()) because it could be list = new ArrayList<D1>();
  • you cannot list.add(new D1()) because it could be list = new ArrayList<D2>();

and so on.

3.2 - Super

By writing
List<? super C2> list =

you are saying that list will be able to reference an object of type (for example) ArrayList whose generic type is one of the 7 supertypes of C2 (C2 included):

  1. new ArrayList<A1>() - can list.add: A1          B1 B2       C1 C2    D1 D2 E1 E2 E3 E4
  2. new ArrayList<A2>() - can list.add:    A2          B2       C1 C2    D1 D2 E1 E2 E3 E4
  3. new ArrayList<A3>() - can list.add:       A3          B3       C2 C3 D1 D2 E1 E2 E3 E4
  4. new ArrayList<A4>() - can list.add:          A4       B3 B4    C2 C3 D1 D2 E1 E2 E3 E4
  5. new ArrayList<B2>() - can list.add:                B2       C1 C2    D1 D2 E1 E2 E3 E4
  6. new ArrayList<B3>() - can list.add:                   B3       C2 C3 D1 D2 E1 E2 E3 E4
  7. new ArrayList<C2>() - can list.add:                            C2    D1 D2 E1 E2 E3 E4

We have a set of “storable” types for each possible case: 7 sets here graphically represented

As you can see, here we have seven safe types that are common to every case: C2D1D2E1E2E3E4.

  • you can list.add(new C2(){}) because, regardless of the kind of List we’re referencing, C2 is allowed
  • you can list.add(new D1(){}) because, regardless of the kind of List we’re referencing, D1 is allowed

and so on. You probably noticed that these types correspond to the hierarchy starting from type C2.