Wildcard Capture & PECS
Wildcards let generic APIs be flexible about producers versus consumers of data — the PECS mnemonic (Producer Extends, Consumer Super) tells you which wildcard to use and why, and wildcard capture explains what the compiler does behind the scenes to make certain operations still type-check.
? extends T and ? super T look similar but express opposite intents: one lets a generic API safely read data out, the other lets it safely put data in. The PECS mnemonic (Producer Extends, Consumer Super) captures the rule for choosing between them, while wildcard capture explains the compiler mechanism that occasionally lets you get around wildcards' natural read/write restrictions.
Why Wildcards Exist — The Invariance Problem
Java generics are invariant: List<Integer> is not a subtype of List<Number>, even though Integer is a subtype of Number. This is intentional (allowing it would let code insert a Double into what's really a List<Integer> through a List<Number>-typed reference, silently corrupting it) — but it means a method taking List<Number> can't accept a List<Integer> argument at all, without wildcards.
static double sumNumbers(List<Number> list) { /* ... */ return 0; }
List<Integer> ints = List.of(1, 2, 3);
// sumNumbers(ints); // compile error: List<Integer> is not a List<Number>Producer Extends — ? extends T
List<? extends Number> means 'a list of some unknown subtype of Number' — the list is a PRODUCER of Number-compatible values for you: you can safely read elements out (each is guaranteed at least a Number), but you cannot safely add anything to it (the compiler doesn't know the list's real, more specific element type, so any add could violate it — except null, which is compatible with every reference type).
static double sumNumbers(List<? extends Number> list) {
double total = 0;
for (Number n : list) { // reading is safe: every element IS-A Number
total += n.doubleValue();
}
return total;
}
List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5);
System.out.println(sumNumbers(ints)); // works now
System.out.println(sumNumbers(doubles)); // works tooConsumer Super — ? super T
List<? super Integer> means 'a list of some unknown supertype of Integer' — the list is a CONSUMER that can safely accept Integer values (or any subtype of Integer) added to it, since whatever the real element type is, it's guaranteed to be Integer or broader. But reading from it only safely gives you Object (the only type guaranteed to be a supertype of every possible actual element type).
static void addNumbers(List<? super Integer> list) {
list.add(1); // safe: any supertype of Integer can accept an Integer
list.add(2);
Object o = list.get(0); // reading only safely gives Object - the real type could be Number or Object
}
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addNumbers(numbers); // works
addNumbers(objects); // works tooPECS — Producer Extends, Consumer Super
PECS is a mnemonic for choosing the right wildcard from the perspective of the data structure passed as a parameter: if your method only reads (produces values out of) the parameter, use ? extends T; if your method only writes (consumes values into) the parameter, use ? super T; if it does both, don't use a wildcard at all — use an exact, invariant type T.
PECS: Producer Extends, Consumer Super
? extends T -> you can only READ T (or a subtype) OUT -> a producer for you
? super T -> you can only WRITE T (or a subtype) IN -> a consumer for you
plain T -> you need to BOTH read and write -> use an exact, invariant typePECS in the JDK — Collections.copy
Collections.copy(List<? super T> dest, List<? extends T> src) is the textbook JDK PECS example: src only needs to be read from (a producer, so ? extends T), and dest only needs to be written to (a consumer, so ? super T). This single signature accepts a much wider range of argument combinations than requiring both lists to be exactly List<T>.
List<Integer> src = List.of(1, 2, 3);
List<Number> dest = new ArrayList<>(List.of(0, 0, 0));
Collections.copy(dest, src); // dest: List<? super Integer>, src: List<? extends Integer> — both fitPECS in the JDK — Comparator
Comparator<? super T> (as seen in List.sort(Comparator<? super T> c)) is another classic consumer-super case: a Comparator only ever consumes two T values (via compare) and produces an int, never a T — so a Comparator<Object> can legitimately be used to sort a List<String>, since it can compare any two objects, Strings included.
List<String> names = new ArrayList<>(List.of("banana", "apple"));
Comparator<Object> byToString = (a, b) -> a.toString().compareTo(b.toString());
names.sort(byToString); // legal: Comparator<Object> is a Comparator<? super String>Capture Conversion
When code has a value of wildcard type like List<?> and passes it to a generic method, the compiler performs capture conversion: internally, it treats the ? as a fresh, specific (though not nameable in source) type variable — often displayed as CAP#1 in compiler diagnostics — for the duration of that expression/call. This is what lets certain code compile that otherwise couldn't, because within that captured scope, the compiler can now reason 'this get() and this set() definitely agree on the same type', even though it still doesn't know WHAT that type actually is.
static void reverse(List<?> list) {
reverseHelper(list); // capture: compiler treats list's element type as a fresh T for this call
}
static <T> void reverseHelper(List<T> list) {
Collections.reverse(list); // fine: both get/set inside operate on the SAME captured T
}Helper Methods for Capture
Because a bare List<?> doesn't let you write to it directly (the compiler can't verify a set matches the unknown type), the standard pattern for operations that need to both read and write an unknown-but-consistent element type is a private generic helper method that the wildcard-typed method delegates to — this forces capture conversion to kick in.
Collections.swap(List<?> list, int i, int j) in the JDK itself uses exactly this pattern internally.
static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j); // delegate for capture
}
private static <T> void swapHelper(List<T> list, int i, int j) {
T temp = list.get(i);
list.set(i, list.get(j));
list.set(j, temp); // safe: all operations agree on the same captured T
}Limitations of Wildcards
? extends T fundamentally cannot support adding non-null elements (the compiler cannot verify what you're adding matches the real, hidden, more specific type), and ? super T fundamentally cannot give you back anything more specific than Object when reading — these are not arbitrary restrictions but direct consequences of not knowing the real type; there is no way to 'unlock' full read+write access to a wildcard type without narrowing it to a specific type parameter first (which is exactly what capture conversion via a helper method achieves for a single call).
List<? extends Number> nums = List.of(1, 2, 3);
// nums.add(5); // compile error: cannot verify 5 (int, boxed) matches the real unknown subtype
List<? super Integer> sink = new ArrayList<Number>();
// Integer first = sink.get(0); // compile error: get() only guarantees Object hereUnbounded Wildcards vs PECS
List<?> (unbounded wildcard, equivalent to List<? extends Object>) is appropriate when a method genuinely doesn't care about the element type at all and only needs type-agnostic operations (size(), isEmpty(), clear(), iterating as Object) — it is more restrictive than ? extends Object in spirit even though technically identical, since 'I don't care about T' usually means you shouldn't be adding non-null elements either.
Edge Cases
add(null) is always legal on any wildcard-typed reference type list, including List<? extends T>, since null is compatible with every reference type regardless of what the real hidden type argument is.
PECS applies to the wildcard's ROLE for that particular API, not to the concept in general — the same conceptual data structure might be a producer in one method and a consumer in another; each parameter's wildcard should be chosen based on how THAT method uses it, not some inherent property of the type.
Mental Model
Picture ? extends T as a sealed box you're only allowed to take things out of (you know each item is at least a T, but you have no idea what else might already be inside, so you can't safely add anything). Picture ? super T as a mail slot you're only allowed to drop things into (you know it accepts T or anything narrower, but you have no idea what you'd actually pull back out, so reading only safely gives you the vaguest possible guarantee — Object). Capture conversion is the compiler momentarily agreeing to treat one specific sealed box as having one definite, if unnamed, label — just long enough to let a helper method swap two items inside it without breaking the seal.
Quick Reference
PECS: Producer Extends, Consumer Super
? extends T : read-only (produces T out); cannot add (except null)
? super T : write-only (consumes T in); reading only gives Object
plain T : both read and write freely; invariant, no subtype flexibility
? (unbounded) : truly don't care about element type; type-agnostic ops only
Capture conversion: compiler treats a used List<?> as List<CAP#1> (a fresh, real,
unnamed type) for one call/expression - enables helper-method patterns
for things like swap/reverse that need matching read+write typesKey Takeaways
Java generics are invariant, so List<Integer> is not a List<Number>; wildcards (? extends/? super) exist to safely relax that rigidity for specific method signatures.
PECS: use ? extends T for a parameter your method only reads from (a producer), ? super T for one it only writes to (a consumer), and plain T when it needs both.
Collections.copy and Comparator<? super T> in the JDK are the standard textbook illustrations of PECS in practice.
Wildcard capture lets the compiler treat a List<?> as a fixed, if unnamed, type for one method call — the standard workaround for operations (like swap/reverse) that need matching read-and-write access.
These are not arbitrary restrictions: ? extends T truly cannot support safe non-null writes, and ? super T truly cannot support reading anything more specific than Object, because the real hidden type argument is unknown in each case.
static void fillWithZero(List<? extends Number> list) { list.add(0); // compile error: cannot add to a ? extends list static void fillWithZero(List<? super Integer> list) { list.add(0); // fine: this is a consumer role, use 'super' per PECS }
Adding elements is a consumer operation (writing into the structure); PECS says a consumer parameter should use '? super T', not '? extends T', which is read-only by nature.
static Number firstElement(List<? super Number> list) { return list.get(0); // compile error: get() on a '? super' list only yields Object static Number firstElement(List<? extends Number> list) { return list.get(0); // fine: reading is a producer role, use 'extends' per PECS }
Reading a specific element type back out is a producer operation; PECS says a producer parameter should use '? extends T', since '? super T' only guarantees Object when read.
static void swapFirstTwo(List<?> list) { Object temp = list.get(0); list.set(0, list.get(1)); // compile error: can't set() on a raw wildcard list.set(1, temp); // compile error too static void swapFirstTwo(List<?> list) { swapHelper(list); } private static <T> void swapHelper(List<T> list) { T temp = list.get(0); list.set(0, list.get(1)); list.set(1, temp); // fine: capture conversion via the helper method's own T }
A bare List<?> cannot support set() directly because the compiler can't prove the value being set matches the hidden real type; delegating to a private generic helper method captures the wildcard as a concrete type parameter, enabling the swap.
Why is List<Integer> not considered a subtype of List<Number> in Java, even though Integer is a subtype of Number?
Java generics are invariant by design: if List<Integer> were treated as a List<Number>, code could insert a Double into it through the List<Number>-typed reference, silently corrupting what is really a List<Integer> and breaking type safety at a later, unrelated point in the program.
What does the PECS mnemonic stand for, and how do you apply it?
Producer Extends, Consumer Super. If a method parameter is only read from (produces data for the method), use ? extends T; if it's only written to (consumes data from the method), use ? super T; if it needs both, use an exact type T with no wildcard.
Why can't you add a non-null element to a List<? extends Number>?
The compiler only knows the list holds SOME unknown subtype of Number — it could really be a List<Integer>, List<Double>, or any other subtype — so it cannot verify that any specific value you try to add (other than null, which fits every type) actually matches that hidden, more specific type.
Why does reading from a List<? super Integer> only give you an Object, not an Integer?
The compiler only knows the list's real element type is SOME unknown supertype of Integer — it could be Number, Object, or Integer itself — and Object is the only type guaranteed to be a supertype of all of those possibilities, so that's the strongest guarantee reading can safely offer.
What is wildcard capture, and why is it needed for a method like Collections.swap(List<?> list, int i, int j)?
Wildcard capture is the compiler internally treating a used ? as one fixed, though unnamed, type variable for the duration of a call. It's needed because a bare List<?> can't support a matching get-then-set (the compiler can't prove they use the same type) — delegating to a private generic helper method captures the wildcard as a real type parameter T, letting both operations agree on the same T and making the swap type-check.
Why does Comparator<? super T> (rather than Comparator<T>) appear in List<T>.sort()'s signature?
A Comparator only consumes T values (via compare) and never produces one, so per PECS it's a consumer and should use ? super T — this lets a broader comparator (e.g. Comparator<Object>) be used to sort a List<String>, since anything that can compare arbitrary Objects can certainly compare Strings too.
Is List<?> the same as List<? extends Object>, and what practical restriction does it carry?
Yes, they are equivalent. In practice it means: you can safely read elements out only as Object, and (aside from null) you cannot add any element to it, since the compiler treats the real element type as completely unknown.
Explain why total() can accept both List<Integer> and List<Double> as arguments, and why total() could not also add elements to nums even if it wanted to.
total(List<? extends Number> nums) accepts any list whose element type is Number or some subtype of Number, so both List<Integer> and List<Double> satisfy that wildcard bound directly — this is exactly the flexibility '? extends' provides that a plain List<Number> parameter would not (List<Integer> is not a List<Number> due to generic invariance). total() could not safely add elements because the compiler doesn't know nums's REAL element type (it could be Integer, Double, or any other Number subtype) — adding, say, a Double to what's actually a List<Integer> at runtime would corrupt it, so the compiler forbids adding any non-null value to a '? extends' typed reference.
Design a method copyOnlyPositive(List<? extends Number> src, List<? super Integer> dest) that copies only the positive int values from src into dest, applying PECS reasoning, with comments justifying each wildcard choice.
import java.util.List; public class PecsDemo { // src: only ever read from -> producer -> '? extends Number' (PECS: Producer Extends) // dest: only ever written to -> consumer -> '? super Integer' (PECS: Consumer Super) static void copyOnlyPositive(List<? extends Number> src, List<? super Integer> dest) { for (Number n: src) { // reading src as Number is always safe int value = n.intValue(); if (value > 0) { dest.add(value); // writing an Integer into dest is always safe } } } public static void main(String[] args) { List<Integer> src = List.of(-2, 3, -1, 5); List<Number> dest = new java.util.ArrayList<>(); copyOnlyPositive(src, dest); System.out.println(dest); // [3, 5] } } src is used purely as a data source (a producer), so per PECS it should use '? extends Number', which lets any List of Number or a subtype (like List<Integer>) be passed, while safely allowing element reads as Number. dest is used purely as a data sink (a consumer) that only ever receives int/Integer values being added, so per PECS it should use '? super Integer', which lets any List whose element type is Integer or a broader supertype (like List<Number> or List<Object>) be passed, while safely allowing Integer values to be added to it.
Explain what capture conversion the compiler performs when compiling static <T> void printSize(List<T> list) { System.out.println(list.size()); } static void printAny(List<?> l) { printSize(l); }, and why this delegation compiles even though l's element type is unknown at the call site.
At the call printSize(l), l has static type List<?> — the compiler doesn't know its real element type. To type-check the call against printSize's signature <T> void printSize(List<T> list), the compiler performs capture conversion: it treats l's wildcard as a fresh, specific (though unnamed in source, often shown as CAP#1 in diagnostics) type, and infers printSize's T to be that captured type. Since printSize only calls list.size() — an operation that needs no knowledge of the actual element type at all — this is trivially safe regardless of what the captured type actually is, so the call compiles cleanly. This same mechanism is what makes helper-method delegation work for read+write operations like swap or reverse, where the captured T lets get() and set() agree on a consistent (if unknown) type.
static double total(List<? extends Number> nums) {
double sum = 0;
for (Number n : nums) sum += n.doubleValue();
return sum;
}
List<Integer> a = List.of(1, 2, 3);
List<Double> b = List.of(1.1, 2.2);
System.out.println(total(a) + total(b));