RizTech Academy logo
RizTech Academy
GenericsLesson 3 of 325 min

Wildcards: reading the signatures libraries give you

Open any Java library and you will find signatures like this:

static <T> void sort(List<T> list, Comparator<? super T> c)
static <T> void copy(List<? super T> dest, List<? extends T> src)

Question marks are the part people skip, and then they cannot call the method they wanted. This lesson makes them ordinary. There are only three forms, and one rule for choosing.

The problem wildcards solve

The previous lesson's invariance rule: List<Tiffins> is not a List<Charge>, even though Tiffins implements Charge.

static long strict(List<Charge> charges) { ... }

List<Tiffins> tiffins = List.of(new Tiffins(26));
strict(tiffins);
error: method strict in class WildBad cannot be applied to given types;
  required: List<Charge>
  found:    List<Tiffins>
  reason: argument mismatch; List<Tiffins> cannot be converted to List<Charge>

The method only reads from the list. It would work perfectly. The type system refuses anyway, because it cannot tell reading from writing — and if it allowed the call, the method could add a Delivery into your List<Tiffins>.

Wildcards let you say which you intend.

? extends — I will read from this

static long totalPaise(List<? extends Charge> charges) {
    long total = 0;
    for (Charge c : charges) {
        total += c.amountPaise();
    }
    return total;
}
totalPaise(tiffins);      // List<Tiffins>
totalPaise(deliveries);   // List<Delivery>
totalPaise(mixed);        // List<Charge>
362340
3500
34940

List<? extends Charge> means "a list of some single unknown type that is Charge or a subtype". Every element is safely a Charge, so reading is fine.

Writing is not:

static void addTo(List<? extends Charge> charges) {
    charges.add(new Tiffins(1));
}
error: no suitable method found for add(Tiffins)
    method List.add(CAP#1) is not applicable
      (argument mismatch; Tiffins cannot be converted to CAP#1)
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Charge from capture of ? extends Charge

CAP#1 is the compiler naming the unknown type so it can talk about it — "capture". The list might really be a List<Delivery>, and adding a Tiffins would corrupt it. The compiler does not know which subtype it is, so it forbids adding anything at all.

? extends gives you a source you can read and cannot write. The only value you can add to one is null, which is true of every type.

? super — I will write into this

static void addDefaults(List<? super Tiffins> destination) {
    destination.add(new Tiffins(26));
    destination.add(new Tiffins(1));
}
List<Charge> charges = new ArrayList<>();
addDefaults(charges);

List<Object> anything = new ArrayList<>();
addDefaults(anything);
[Tiffins[count=26], Tiffins[count=1]]
[Tiffins[count=26], Tiffins[count=1]]

List<? super Tiffins> means "a list of some unknown type that is Tiffins or a supertype". Whatever it really is, a Tiffins is a valid element, so adding is safe.

Reading is the part you lose:

static Charge readFrom(List<? super Charge> charges) {
    return charges.get(0);
}
error: incompatible types: CAP#1 cannot be converted to Charge
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object super: Charge from capture of ? super Charge

The list might be a List<Object>, so an element is only guaranteed to be an Object. That is all you get back.

The rule: PECS

Producer extends, Consumer super.

Ask what the parameter does from the method's point of view:

  • It produces values you read → ? extends T
  • It consumes values you write → ? super T
  • It does both → no wildcard, plain List<T>
static <T> void copy(List<? extends T> source, List<? super T> destination) {
    for (T item : source) {
        destination.add(item);
    }
}

Both in one signature: read from source, write into destination. That is Collections.copy, and now its declaration reads as prose.

The standard library is full of these once you know the rule:

Signature Why
list.sort(Comparator<? super E>) A comparator for a supertype can compare subtype elements
stream.map(Function<? super T, ? extends R>) Reads T, produces R
Optional.ifPresent(Consumer<? super T>) The consumer only receives values
list.addAll(Collection<? extends E>) The source is read from
Collectors.toMap(Function<? super T, ? extends K>, ...) Same pattern again

sort is the one worth understanding, because it is the one you will meet first:

Comparator<Charge> byAmount = Comparator.comparingLong(Charge::amountPaise);

List<Tiffins> sortable = new ArrayList<>(tiffins);
sortable.sort(byAmount);
[Tiffins[count=18], Tiffins[count=26]]

A Comparator<Charge> sorting a List<Tiffins>. Without ? super E in the signature, that call would be rejected and you would need a separate comparator per subtype.

? on its own

static int size(List<?> anything) {
    return anything.size();
}

List<?> is "a list of some unknown type" — equivalent to List<? extends Object>. You can read elements as Object, ask the size, and iterate. You cannot add anything except null.

Use it when the element type genuinely does not matter. It is also the correct replacement for a raw List, and the difference is real: a raw List lets you add anything and silently disables generic checking on every method you call on it; List<?> is checked and safe.

Where wildcards do not belong

Not on return types. List<? extends Charge> getCharges() forces every caller to deal with a wildcard they did not ask for, and they cannot add to the result. Return List<Charge>.

Not on a type parameter you control. If you are writing <T extends Charge> long total(List<T> charges) and T appears only once, you did not need the type parameter — List<? extends Charge> says the same thing more simply. The reverse also holds: if the same unknown type must appear twice in the signature, a wildcard cannot express it and you need a named T.

Not for their own sake. Inside a method body, local variables rarely want them. Wildcards are a tool for parameters in APIs that other code calls.

A summary table

Form Read as Can read Can add Use for
List<Charge> Exactly charges Charge Charge Both reading and writing
List<? extends Charge> Charges or a subtype Charge Only null A source
List<? super Charge> Charges or a supertype Object Charge A destination
List<?> Anything Object Only null When the type is irrelevant
List Raw — avoid Object Anything Never

Check your work

Why is List<Tiffins> rejected where List<Charge> is required? Generics are invariant. If it were allowed, the method could add a Delivery into your list of tiffins.

What does ? extends Charge allow and forbid? Reading every element as a Charge; adding anything except null, because the actual element type is unknown.

What does ? super Tiffins allow and forbid? Adding a Tiffins or any subtype; reading anything more specific than Object.

What is CAP#1 in an error message? The compiler's name for the captured unknown type behind a wildcard, so it can explain why your call does not fit.

State PECS. Producer extends, consumer super. If the parameter produces values you read, use extends; if it consumes values you write, use super; if both, no wildcard.

Why does List.sort take Comparator<? super E>? So a comparator written for a supertype can sort a list of subtypes — one Comparator<Charge> sorts List<Tiffins> and List<Delivery> alike.

What is wrong with a wildcard in a return type? It pushes the wildcard onto every caller, who then cannot add to the result and gains nothing.

Why is List<?> better than a raw List? Both prevent you adding; only the raw type also disables generic checking on everything else you do with it.

Practice 3, PECS applied. totalPaise reads, so List<? extends Charge>. addDefaults writes, so List<? super Tiffins>. copy does both, one each way. A method that reads and writes the same list needs a plain List<T> — there is no wildcard that permits both, and that is the rule doing its job rather than a gap.

Practice 5, the return type. Changing getCharges() to return List<? extends Charge> compiles, and then every caller who wants to add a charge to the result cannot, and gets the CAP#1 error for a restriction the method never intended. Wildcards belong on parameters.

Practice

  1. Reproduce the rejection. A method taking List<Charge>, called with a List<Tiffins>. Read the error. Then add ? extends and confirm all three list types now work.

  2. Break both wildcards. Try to add to a ? extends parameter and to read a specific type from a ? super one. Read both messages and find CAP#1 in each.

  3. Apply PECS three times. Write totalPaise, addDefaults and copy from scratch and choose the wildcard for each before looking back. Then try to write one method that both reads and writes the same list, and work out which wildcard lets you — the answer is neither.

  4. Sort with a supertype comparator. Write Comparator<Charge> and sort a List<Tiffins> with it. Then change your own method's signature from Comparator<E> to Comparator<? super E> and see which calls become possible.

  5. Put a wildcard where it does not belong. Change a getter to return List<? extends Charge> and then try to use the result in a caller. Write one sentence on what the caller lost.

  6. Harder — read the real thing. Open the Javadoc or the source for Collections.max, Stream.map and Collectors.groupingBy and write each signature out by hand. For every ? extends and ? super, say in one phrase whether that parameter is a producer or a consumer. If a signature still does not make sense after that, it is almost always because the same type appears twice and needs a named parameter rather than a wildcard.


That is module four. The angle brackets are no longer noise: you can read a library signature, write a generic class and method, choose a bound, and pick the right wildcard for a parameter.

Next module: collections — where all of this becomes daily work, and where equals and hashCode have a contract you must not break.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship