Bounded types and wildcards

extends, super, PECS, and how to read List<? super Integer> without guessing.

9 min read🗂️ Collections and Generics

The previous lesson ended with a wall: List<Integer> cannot be used where a List<Number> is expected. Wildcards are the door in that wall. They let a method accept "a list of some subtype of Number" or "a list of some supertype of Integer", and the rule for which one you want fits in four letters. This lesson is that rule, what the compiler does with the unknown type it stands for (it has a name, and you have seen it in error messages), and how to read the JDK's most intimidating signatures in one pass.

The problem

java
static double sum(List<Number> xs) { ... }
 
sum(List.of(1, 2, 3));            // does not compile: List<Integer> is not List<Number>

sum only reads from the list. Reading an Integer where a Number is expected is safe. The signature is too strict.

? extends: a producer you read from

java
static double sum(List<? extends Number> xs) {
    double total = 0;
    for (Number n : xs) total += n.doubleValue();    // reading as Number: safe
    return total;
}
sum(List.of(1, 2, 3));              // List<Integer>: fine
sum(List.of(1.5, 2.5));             // List<Double>: fine

List<? extends Number> means "a list whose element type is Number or some subtype — I do not know which". You can take elements out as Number. You cannot put anything in (except null), because the compiler does not know whether the list is really a List<Integer> or a List<Double>:

java
xs.add(3);          // does not compile: xs might be a List<Double>

? super: a consumer you write to

java
static void fillWithZeros(List<? super Integer> xs, int n) {
    for (int i = 0; i < n; i++) xs.add(0);          // writing an Integer: safe
}
List<Number> nums = new ArrayList<>();
List<Object> objs = new ArrayList<>();
fillWithZeros(nums, 3);             // Number is a supertype of Integer: fine
fillWithZeros(objs, 3);             // so is Object

List<? super Integer> means "a list whose element type is Integer or some supertype". You can put an Integer in, because whatever the real type is, an Integer is one. What you get out is only an Object, because the compiler does not know which supertype it is.

Under the hood: capture, and what the wildcard costs

A wildcard is not a type; it is a constraint on an unknown type. When you use a List<? extends Number>, the compiler performs capture conversion (JLS §5.1.10): it invents a fresh type variable for the unknown, writes it as CAP#1 in its messages, records its bound (CAP#1 extends Number), and type-checks the expression as if the list were a List<CAP#1>. That is the whole explanation of the two rules:

  • xs.get(0) has type CAP#1, which is assignable to Number because of the bound. Reading works.
  • xs.add(3) needs an Integer to be a CAP#1. Nothing is known to be a CAP#1 except null. Writing fails, and the error says so: incompatible types: int cannot be converted to CAP#1 where CAP#1 is a fresh type-variable.

For ? super Integer the capture is CAP#1 super Integer: an Integer is assignable to it (write works), and a CAP#1 is only known to be an Object (read gives Object). Every wildcard rule is capture plus one bound.

At run time none of this exists. Erasure turns List<? extends Number> into List, and a read out of it compiles to a checkcast Number, the bound; a read out of List<? super Integer> needs no cast at all. Wildcards are free at run time; their entire cost and benefit is in the compiler.

Bounds can also be recursive. <E extends Enum<E>> says "an enum type, compared with itself", which is what EnumSet.of needs; <T extends Comparable<? super T>> is the next section. These read as constraints on the unknown, exactly like a wildcard, and the compiler solves them by inference at the call site.

PECS

Producer extends, Consumer super.

  • If a parameter produces values your code reads → ? extends T.
  • If a parameter consumes values your code writes → ? super T.
  • If it does both → an exact T, no wildcard.
  • If you neither read as T nor write T? alone.

The JDK is written this way. Collections.copy(List<? super T> dest, List<? extends T> src) reads from src and writes to dest. Stream.map(Function<? super T, ? extends R>) takes a function that accepts a T (consumer) and returns an R (producer). Once you know PECS, those signatures read as English.

Walkthrough: reading Collectors.toMap in one pass

java
public static <T, K, U> Collector<T, ?, Map<K, U>> toMap(
        Function<? super T, ? extends K> keyMapper,
        Function<? super T, ? extends U> valueMapper)

People give up at this signature. Take it left to right, applying one rule per part:

  1. <T, K, U>: three unknowns, inferred at the call. T is the stream's element type; K and U are the map's key and value types.
  2. Collector<T, ?, Map<K, U>>: a collector that consumes Ts and produces a Map<K, U>. The ? in the middle is the accumulation type, which callers never see, so it is an unbounded wildcard: "there is one, you do not need to know it".
  3. Function<? super T, ? extends K> keyMapper: PECS on both parameters of Function. The function consumes a T (so any function that accepts a supertype of T will do: a Function<Object, K> can map a T) and produces a K (so a function returning a subtype of K will do). Without the wildcards, toMap(Person::getName, ...) on a Stream<Employee> would fail because Person::getName is a Function<Person, String>, not a Function<Employee, String>.
  4. valueMapper: the same shape for U.
  5. So employees.stream().collect(toMap(Person::getName, Employee::getSalary)) infers T = Employee, K = String, U = Integer, and every wildcard is what lets a method reference declared on a supertype or returning a subtype fit.

The rule for reading any such signature: type parameters first, then the return type, then each parameter as "consumes what, produces what". The wildcards are never the point; they are the slack that makes the point usable.

Reading Comparable<? super T>

java
static <T extends Comparable<? super T>> T max(Collection<? extends T> xs)

T extends Comparable<? super T>: T can be compared to itself or to a supertype of itself. Why not Comparable<T>? Because java.sql.Timestamp extends java.util.Date and implements Comparable<Date>, not Comparable<Timestamp>. The ? super bound accepts it. Collection<? extends T> is PECS: the collection produces elements.

You will not write this signature often. You will read it whenever you open Collections, Comparator or Stream, and being able to parse it in one pass is the skill.

The unbounded wildcard

List<?> is "a list of something". You can read Objects and check size() and isEmpty(); you cannot add. Use it when the element type is genuinely irrelevant — printAll(List<?> xs) — and prefer it to the raw List, which turns checking off entirely. instanceof List<?> is the only reifiable form and the one to write in a type check.

Wildcards are for parameters, not fields or returns

A method parameter with a wildcard makes the method more accepting: good. A return type with a wildcard forces every caller to deal with the wildcard: bad — return List<T>. A field typed List<? extends Foo> can never be added to: almost always wrong. Local variables rarely need them either. Wildcards live in method signatures on the input side.

Wildcard capture

Occasionally you need to name the unknown type — to swap two elements of a List<?>, say. The trick is a helper method with a type parameter:

java
static void swapFirstTwo(List<?> xs) { swapHelper(xs); }
private static <T> void swapHelper(List<T> xs) {
    T tmp = xs.get(0);
    xs.set(0, xs.get(1));
    xs.set(1, tmp);
}

The compiler captures the wildcard as T for the helper call: CAP#1 gets a name you can use, and inside the helper both the read and the write are the same T, so the swap type-checks. This is rare in application code; recognise it when you see it in a library, and recognise the error message that sends you to it: required: CAP#1, found: Object.

Try it yourself

Which lines compile?

java
List<? extends Number> a = new ArrayList<Integer>();
List<? super Integer> b = new ArrayList<Number>();
Number n1 = a.get(0);      // 1
a.add(1);                  // 2
a.add(null);               // 3
b.add(1);                  // 4
Integer i = b.get(0);      // 5
Object o = b.get(0);       // 6
Answer

1, 3, 4, 6 compile; 2 and 5 do not. a is a List<CAP#1 extends Number>: reading gives a Number (1); writing needs a CAP#1, which only null is (3 yes, 2 no). b is a List<CAP#2 super Integer>: an Integer is a CAP#2 (4); reading gives a CAP#2, known only to be an Object (6 yes, 5 no). Every answer is capture plus the bound.

Fix the signature

java
static <T> void copyAll(Collection<T> from, Collection<T> to) { for (T t : from) to.add(t); }
List<Integer> ints = List.of(1, 2);
List<Number> nums = new ArrayList<>();
copyAll(ints, nums);        // does not compile

Rewrite copyAll so the call works, and say what T is inferred as.

Answer

static <T> void copyAll(Collection<? extends T> from, Collection<? super T> to). from produces, to consumes: PECS. With the call, inference picks T = Integer (or Number; both satisfy the bounds), List<Integer> fits ? extends T, and List<Number> fits ? super T. This is Collections.copy's signature, and Collections.addAll's.

Read it aloud

java
public static <T> Comparator<T> comparing(Function<? super T, ? extends Comparable> keyExtractor)

(simplified from Comparator.comparing). Say what each wildcard permits, and why Comparator.comparing(Person::getName) works on a Comparator<Employee>.

Answer

? super T: the key extractor may accept any supertype of T, so a function declared on Person can serve a Comparator<Employee>. ? extends Comparable: the key may be any type that is comparable, so String, LocalDate or your own value type all fit. Without the first wildcard, Person::getName would be a Function<Person, String> and not assignable to Function<Employee, String>; with it, the compiler captures CAP#1 super Employee and Person satisfies it.

Misconceptions

  • "List<? extends Number> is a list of any numbers." It is a list of one unknown subtype of Number. That is why you cannot add: the compiler does not know which.
  • "? super means you can read supertypes out." You can only read Object out. super is for writing; the read side collapses to the top type.
  • "Wildcards cost something at run time." They erase to the raw type like everything else; the only artefact is a checkcast to the bound on reads through extends.
  • "Wildcards belong everywhere generics do." On method parameters. In fields and return types they push the unknown onto every user.
  • "CAP#1 in an error means the compiler is confused." It is the captured wildcard's name. The message is telling you exactly which unknown type the expression needed and did not have.

Going deeper

  • JLS §4.5.1 (wildcards) and §5.1.10 (capture conversion), the source of CAP#1.
  • JLS §18, type inference, for how T is chosen at a call site with wildcards on both sides.
  • Effective Java, item 31 (use bounded wildcards to increase API flexibility), where PECS was coined.
  • The signatures of Collections.copy, Collections.max, Comparator.comparing, Stream.map and Collectors.toMap, read with the five-step method above.
  • Angelika Langer's Java Generics FAQ, the "wildcard capture" section.
Progress is saved on this device and to your account when signed in.