Wildcards: ? extends / ? super (PECS)
Producer-extends, consumer-super — covariance and contravariance for generic parameters.
Generics are invariant
The surprising rule first: List<Integer> is not a subtype of List<Number>, even though Integer is a subtype of Number. Generic types are invariant. The reason is safety — if it were allowed:
List<Integer> ints = new ArrayList<>();
List<Number> nums = ints; // NOT allowed (pretend it were)
nums.add(3.14); // a Double into a List<Integer>?! would corrupt ints
To regain flexibility *safely*, use wildcards — the ? type, optionally bounded.
? extends — an upper bound (a producer)
List<? extends Number> means 'a list of some unknown type that is Number or a subtype'. You can pass a List<Integer> or List<Double> to it. Because every element IS-A Number, you can safely read elements as Number:
static double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) { // safe: each element is at least a Number
total += n.doubleValue();
}
return total;
}
sum(List.of(1, 2, 3)); // List<Integer> — accepted
sum(List.of(1.5, 2.5)); // List<Double> — accepted
But you cannot add to a List<? extends Number> (except null): the compiler does not know the exact element type, so it cannot verify that what you add is legal. ? extends is a producer — you take values out.
? super — a lower bound (a consumer)
List<? super Integer> means 'a list of some unknown type that is Integer or a *supertype*'. You can pass a List<Integer>, List<Number>, or List<Object>. Because the list accepts Integer or anything broader, you can safely add Integers:
static void addInts(List<? super Integer> dst) {
dst.add(1); // safe: dst holds Integer or a supertype
dst.add(2);
}
addInts(new ArrayList<Number>()); // accepted
addInts(new ArrayList<Object>()); // accepted
But reading back only gives you Object (the one type the unknown supertype is guaranteed to be). ? super is a consumer — you put values in.
PECS: Producer Extends, Consumer Super
The rule, from Joshua Bloch: when a parameter both produces and consumes you would pick based on its role. A copy method needs both:
static <T> void copy(List<? super T> dst, List<? extends T> src) {
for (T item : src) { // src PRODUCES T → extends
dst.add(item); // dst CONSUMES T → super
}
}
src produces (extends), dst consumes (super). This signature lets you copy a List<Integer> into a List<Number> — maximally flexible and still type-safe.