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
-
Reproduce the rejection. A method taking
List<Charge>, called with aList<Tiffins>. Read the error. Then add? extendsand confirm all three list types now work. -
Break both wildcards. Try to
addto a? extendsparameter and to read a specific type from a? superone. Read both messages and findCAP#1in each. -
Apply PECS three times. Write
totalPaise,addDefaultsandcopyfrom 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. -
Sort with a supertype comparator. Write
Comparator<Charge>and sort aList<Tiffins>with it. Then change your own method's signature fromComparator<E>toComparator<? super E>and see which calls become possible. -
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. -
Harder — read the real thing. Open the Javadoc or the source for
Collections.max,Stream.mapandCollectors.groupingByand write each signature out by hand. For every? extendsand? 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