Skip to content
Part IA Michaelmas Term

Wildcards and PECS

Generic invariance is safe but restrictive. A method accepting List<Animal> cannot receive a List<Dog>, even if it only reads from the list. Wildcards restore controlled flexibility without sacrificing type safety.

Generics Bounded Wildcards & PECS

Three Wildcard Forms

1. Upper-Bounded Wildcard: List<? extends T>

“An unknown type that is T or a subtype of T.”

void printAnimals(List<? extends Animal> animals) {
    for (Animal a : animals) {
        System.out.println(a);  // safe to READ — every element is at least an Animal
    }
    animals.add(new Dog());     // COMPILE ERROR — cannot add (except null)
}

You can safely read T (or any supertype of T) from the list, because every element is known to be some subtype of T.

You cannot add elements (except null) because the actual element type is unknown — it could be Dog, Cat, or anything else extending Animal. Adding a Dog would be unsafe if the list is actually a List<Cat>.

2. Lower-Bounded Wildcard: List<? super T>

“An unknown type that is T or a supertype of T.”

void addDogs(List<? super Dog> animals) {
    animals.add(new Dog());     // safe to WRITE — a Dog is always a valid element
    animals.add(new Puppy());   // also safe — Puppy extends Dog
    Animal a = animals.get(0);  // COMPILE ERROR — get() returns Object (or lower-bound capture)
}

You can safely write T (or any subtype of T) into the list, because the actual type is some supertype of Dog that can hold any Dog.

You cannot read Animal or Dog back out — the compiler only guarantees Object (the common supertype of all reference types). The actual type could be Object, Animal, Mammal, or Dog — the compiler conservatively returns Object.

3. Unbounded Wildcard: List<?>

“A list of some completely unknown type.”

int countElements(List<?> list) {
    return list.size();    // safe — size() does not depend on element type
}

You can call methods that do not depend on the type parameter: size(), clear(), contains(Object), remove(Object), iterator(). You cannot add elements (except null). Reads return Object.

This is useful when you genuinely do not care about the element type — your code only uses collection-agnostic operations.

PECS: Producer Extends, Consumer Super

This mnemonic, from Josh Bloch’s Effective Java, helps determine which wildcard to use:

Producer Extends, Consumer Super.

A producer is something that gives values to you — you read from it. Use ? extends T.

A consumer is something that receives values from you — you write to it. Use ? super T.

If something is both a producer and a consumer, use the exact type (T) — no wildcard.

Canonical Example: Collections.copy()

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (int i = 0; i < src.size(); i++) {
        dest.set(i, src.get(i));
    }
}
  • src is a producer — we read T from it. Bounded ? extends T.
  • dest is a consumer — we write T into it. Bounded ? super T.

This signature allows copying a List<Integer> into a List<Number>, or a List<Dog> into a List<Animal>, while maintaining type safety:

List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = new ArrayList<>(Arrays.asList(0.0, 0.0, 0.0));
Collections.copy(nums, ints);  // dest: List<Number>, src: List<Integer> — valid

Wildcard Capture

When you access a wildcard-typed variable, the compiler performs capture — it invents a fresh, anonymous type variable to represent the unknown type:

void swapFirstTwo(List<?> list) {
    Object first = list.get(0);
    list.set(0, list.get(1));  // COMPILE ERROR — set requires the captured type
    list.set(1, first);        // COMPILE ERROR — same reason
}

Even though pulling an element out and putting it back seems type-safe, the compiler cannot prove it. The capture type from get(1) is a different fresh type than the capture type required by set(0, ...), and the compiler conservatively rejects the mismatch.

The fix is a helper method that names the type:

void swapFirstTwo(List<?> list) {
    swapHelper(list);
}

private <T> void swapHelper(List<T> list) {
    T first = list.get(0);
    list.set(0, list.get(1));
    list.set(1, first);
}

Once T is a named type parameter, the compiler can verify that get(0) and set(1, ...) operate on the same type.

Wildcards Are for Method Parameters, Not Return Types

Returning a wildcard type from a method forces the caller to deal with an unknown type — avoid this:

List<? extends Animal> getAnimals() { ... }  // avoid — caller cannot add elements

The caller cannot add to the returned list or access elements at a useful type. Prefer returning the exact type (List<Animal>) or parameterising the method so the caller’s type flows through naturally (List<T> on a generic class).

Common Tripos Question Patterns

  1. Trace wildcard code: given method signatures with wildcards and a sequence of calls, determine which lines compile and which produce type errors. Trace the capture reasoning.

  2. Apply PECS: given a description of how a parameter is used, choose the correct wildcard declaration from a set of options.

  3. Wildcard capture errors: identify why a particular use of ? is too restrictive and how a helper method with a named type parameter resolves it.

  4. Compare with arrays: given equivalent array and generic code, explain why the array version compiles but fails at runtime while the generic version fails at compile time.