9.4
Wildcards: `extends` and `super`
A Dog is an Animal, but a List of Dogs is not a List of Animals. That refusal protects you, and wildcards are how you work around it without losing the protection.
Previously on
Section 9.3 gave you generics: one class that works with any type, checked at compile time instead of crashing at run time.
It ended on a question. A Dog is an Animal, so is a List<Dog> a List<Animal>?
The answer is no. This section is why, and what to do about it.
The problem
Inheritance from Section 7.1 works exactly as you expect:
Animal a = new Dog(); // fine. A Dog is an Animal.Now put them in a list:
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs; // ERRORincompatible types: List<Dog> cannot be converted to List<Animal>Java says no. And the reason is that saying yes would let you break the list.
What would happen if Java allowed it
- List<Animal> animals = dogs;Both variables now point at the same list. That is Section 6.3: assignment copies the reference.
- animals.add(new Cat());Completely legal.
animalsis a list of Animals, and a Cat is an Animal. - Dog d = dogs.get(1);Crash. There is a Cat in the list everybody believed held only Dogs.
Java blocks step one so step three can never happen. The relationship between Dog and Animal does not carry over to List<Dog> and List<Animal>.
The word for that is invariant.
Which leaves you with a real problem. How do you write one method that adds up any list of numbers?
static double sum(List<Number> nums) { ... }
sum(List.of(1, 2, 3)); // ERROR. That is a List<Integer>, not a List<Number>.
sum(List.of(1.5, 2.5)); // ERROR. That is a List<Double>.The method is useless. It accepts exactly one type of list, and nobody has one.
The idea
The answer is the wildcard, written ?. It means “some type I am not naming”.
? extends when you read
static double sum(List<? extends Number> nums) {
double total = 0;
for (Number n : nums) total += n.doubleValue();
return total;
}
sum(List.of(1, 2, 3)); // 6.0
sum(List.of(1.5, 2.5)); // 4.0List<? extends Number> means “a list of some one type, and that type is a Number or below”. A List<Integer> fits. So does a List<Double>.
Reading is safe. Whatever the real list holds, every element is at least a Number, so n.doubleValue() always works.
Writing is not:
List<? extends Animal> list = dogs;
list.add(new Dog()); // ERRORThe compiler does not know whether the real list is a List<Dog> or a List<Cat>. Adding a Dog would be wrong in the second case, so it refuses everything.
? super when you write
static void addDogs(List<? super Dog> list) {
list.add(new Dog());
list.add(new Dog());
}
addDogs(new ArrayList<Dog>()); // works
addDogs(new ArrayList<Animal>()); // works
addDogs(new ArrayList<Object>()); // worksList<? super Dog> means “a list of some type that is a Dog or above”. A list of Dogs, Animals or Objects all qualify.
Writing is safe here. Whatever the real list holds, a Dog is acceptable, because the list’s type is Dog or something more general.
Reading gives you very little:
List<? super Dog> list = animals;
Object o = list.get(0); // Object is all you get
Dog d = list.get(0); // ERRORThe list might really hold Object. The only thing everything is, is an Object.
Under the hood
Going deeperThe three shapes side by side
| Declaration | What you can do | |
|---|---|---|
| List<Animal> | exactly this type. Invariant. | read Animal, write Animal |
| List<? extends Animal> | Animal or below. Covariant. | read Animal. Cannot write. |
| List<? super Dog> | Dog or above. Contravariant. | write Dog. Read only Object. |
| List<?> | any list at all | read Object. Cannot write. |
Those three words are the proper names, and they are worth knowing because they turn up in every discussion of this:
- Invariant: no relationship.
List<Dog>andList<Animal>are unrelated types. - Covariant: the relationship follows the elements.
List<? extends Animal>accepts aList<Dog>. - Contravariant: the relationship runs backwards.
List<? super Dog>accepts aList<Animal>.
PECS
There is a rule that tells you which to use, and it is four letters.
PECS: Producer
extends, Consumersuper.
Ask one question about the parameter: is it giving me values, or am I giving it values?
| The parameter is a | So | |
|---|---|---|
| Producer | it gives you values. You read. | ? extends |
| Consumer | it takes values. You write. | ? super |
| Both | you read and write | no wildcard. Use the exact type. |
| Neither | you only need any list | List<?> |
Java’s own library follows it exactly. Here is a real signature:
static <T> void copy(List<? super T> dest, List<? extends T> src)The source produces the elements, so it is extends. The destination consumes them, so it is super. Both halves of the rule, in one method, and now it reads as sense rather than noise.
When to use T and when to use ?
This is the part that confuses people, and the distinction is simple once stated.
Use T when you need to name the type more than once.
static <T> void copy(List<? super T> dest, List<? extends T> src)T appears twice, and the two uses must agree. A wildcard cannot express that, because each ? is a separate unknown.
Use ? when you only mention the type once and do not care what it is.
static void printAll(List<?> list) {
for (Object o : list) System.out.println(o);
}Naming a type parameter here would add nothing.
? is not Object
List<Object> and List<?> look similar and behave very differently.
List<Object> a = new ArrayList<Dog>(); // ERROR. Invariant.
List<?> b = new ArrayList<Dog>(); // fineList<Object> is a list whose element type is Object. Only a List<Object> fits.
List<?> is a list of some unknown type. Any list fits. You just cannot add to it, because nobody knows what type it would have to be.
Wildcards do not exist at run time either
Everything here is checked by the compiler and then erased, exactly as in Section 9.3. At run time all of these are plain List.
So a wildcard never costs a single instruction. It is a rule for javac, and nothing more.
What it costs
Signatures get hard to read. Collections.copy(List<? super T> dest, List<? extends T> src) carries real meaning, and most people skip past it rather than decode it.
The rules also feel backwards at first. extends gives you a list you can only read, and super gives you one you can only write. Both are correct, and both are the opposite of what the words suggest.
Error messages are worse here than anywhere else in Java. CAP#1 means nothing until somebody explains it once.
And you have to decide up front. Wildcards go in the method signature, so choosing wrong means changing the signature and every caller.
What you get is one method that works for every list of a family, with no casts and no run time risk. Without wildcards, sum(List<Number>) would accept nothing anybody actually has, and the whole Collections Framework would be far clumsier to use.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
A `Dog` is an `Animal`. Why is a `List<Dog>` not a `List<Animal>`?
Show the answer
Because if it were, you could break the list of Dogs.
Imagine it were allowed. You write
List<Animal> animals = dogs;. Nowanimalsanddogspoint at the same list. Then somebody writesanimals.add(new Cat()), which is perfectly legal for a list of Animals.Now the list that everybody believes holds only Dogs has a Cat in it. The next line to read a Dog out of it fails.
Java stops the first line so the last one can never happen. Generics are invariant:
List<Dog>andList<Animal>have no relationship at all, even though Dog and Animal do.Why can you read from a `List<? extends Animal>` but not add to it?
Show the answer
Reading is safe. Whatever the real list holds, every element is at least an Animal. So
Animal a = list.get(0)always works.Writing is not. The list might really be a
List<Dog>, or aList<Cat>. The compiler does not know which. Adding a Dog would be wrong if it is really a list of Cats, so it refuses everything.The error message is odd and worth recognising:
D cannot be converted to CAP#1.CAP#1is the compiler's name for "the one specific type this really is, which I cannot see".So
? extendsgives you a list you can read from. That is all, and it is often all you need.State the PECS rule and explain what it is short for.
Show the answer
Producer extends, Consumer super.
If the parameter produces values for you, you are reading from it, so use
? extends.If the parameter consumes values you give it, you are writing into it, so use
? super.You can see both halves in one real signature:
Collections.copy(List<? super T> dest, List<? extends T> src). The source produces, so it isextends. The destination consumes, so it issuper.If you neither read nor write, and only want any list at all, use plain
List<?>.
Exercises
Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.
4 exercises100 pointsabout 100 minutes
Break It If Java Let You
ex-9-4-aJava refuses one line. Work out what disaster that refusal prevents.
Start by noticing that the single object assignment works fine. Animal a = d compiles. So the refusal is not about Dogs and Animals, it is about lists of them.
Uncomment the three lines and read the error. Then write out the story: if step one were allowed, step two is legal, and step three crashes.
Once you can tell that story, invariance stops being an arbitrary rule and becomes something Java is doing for you.
What your program must do
- Explain why Animal a = d compiles but List<Animal> animals = dogs does not
- Uncomment the assignment and copy the exact compiler error
- Write out the three step story of what would break if Java allowed it
- Say what the word invariant means, in your own words
import java.util.*;
class Animal { public String toString() { return "Animal"; } }
class Dog extends Animal { public String toString() { return "Dog"; } }
class Cat extends Animal { public String toString() { return "Cat"; } }
public class Invariant {
public static void main(String[] args) {
Dog d = new Dog();
Animal a = d; // fine. Why?
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
// List<Animal> animals = dogs; // uncomment: read the error
// animals.add(new Cat()); // this is what it prevents
// Dog first = dogs.get(1); // and this is what would break
// TODO: write the three lines out as a comment explaining the danger
}
}
Hint 1
Dog being an Animal is inheritance, from Section 7.1. Lists are a different question: it is about whether List<Dog> is a List<Animal>.Hint 2
Hint 3almost the answer
Dog and Animal are related. List<Dog> and List<Animal> are two unrelated types, and Java keeps them that way on purpose.Read With extends
ex-9-4-bWrite a method that adds up any list of numbers, and find out why the obvious version is useless.
sumStrict compiles perfectly and nobody can call it, because nobody has a List<Number>. That is worth seeing before you learn the fix.
Then write the wildcard version and call it with three different list types.
Finish by trying to add something inside the method. The error mentions CAP#1, which means nothing until somebody explains it once. Copy the message down. You will meet it again.
What your program must do
- Show that sumStrict cannot be called with a List<Integer>
- Write sumAny using a wildcard so all three list types work
- Try to add an element inside sumAny and copy the exact error
- Explain why reading is allowed and writing is not
import java.util.*;
public class Reading {
// This compiles. Now try to call it.
static double sumStrict(List<Number> nums) {
double t = 0; for (Number n : nums) t += n.doubleValue(); return t;
}
// TODO: sumAny(...) that accepts List<Integer>, List<Double>, List<Long>
public static void main(String[] args) {
// System.out.println(sumStrict(List.of(1, 2, 3))); // uncomment: error
// TODO: call your sumAny with three different list types
// TODO: try to add to the wildcard list inside the method. Read the error.
}
}
Hint 1
List<Number> means exactly that type. A List<Integer> is a different, unrelated type, so nothing you have fits.Hint 2
List<? extends Number> means a list of some one type, at or below Number. Reading gives you a Number every time, whatever the real type is.Hint 3almost the answer
CAP#1. That is the compiler's name for the specific type it cannot see. The list might really be a List<Integer>, so adding a Double would be wrong, so it refuses everything.Write With super
ex-9-4-cThe other half of PECS, and the one most people never quite learn.
Start with addThreeDogs. Get it accepting a List<Dog>, a List<Animal> and a List<Object> with one signature.
Then read from that list and look at what type comes back. It is Object, and working out why is the whole idea: the list might really hold anything above Dog.
Finish with copy. It is the real signature from java.util.Collections, and it has both halves of PECS in one line. Once you can explain why each parameter uses the wildcard it does, you can read almost any generic signature in the standard library.
What your program must do
- Write addThreeDogs so all three list types are accepted
- Show what type you get when you read from a ? super Dog list
- Write copy with the PECS signature and use it
- Say which half of PECS applies to each parameter of copy, and why
import java.util.*;
class Animal { public String toString() { return getClass().getSimpleName(); } }
class Dog extends Animal { }
class Puppy extends Dog { }
public class Writing {
// TODO: addThreeDogs(...) that works on List<Dog>, List<Animal> AND List<Object>
// TODO: copy(dest, src) with the PECS signature. Both halves in one method.
public static void main(String[] args) {
// TODO: call addThreeDogs with all three list types
// TODO: read from a ? super Dog list and see what type you get back
// TODO: use your copy() to move a List<Dog> into a List<Animal>
}
}
Hint 1
List<? super Dog> means a list of Dog or something above it. Adding a Dog is always safe, because the list's real type is Dog or more general.Hint 2
? super Dog gives you Object and nothing better. The list might really hold Object, and the only thing everything is, is an Object.Hint 3almost the answer
static <T> void copy(List<? super T> dest, List<? extends T> src). The source produces values, so extends. The destination consumes them, so super.Pick the Right Signature
ex-9-4-dFive methods for a real API. Every one has a right signature, and a reason.
Do not guess. Ask PECS each time: is this parameter giving me values, or am I giving it values? Then apply the second test for T against ?: write it with T, and if T appears only once, replace it.
Number 5 is the one that separates people who know the rule from people who memorised it. firstOrNull has to return the caller’s exact type, so a List<AirUnit> gives back an AirUnit rather than a Unit. A wildcard alone cannot express that.
Write your reason for each as a comment. Six months later that comment is the only thing that will explain the signature.
What your program must do
- Write all five signatures and justify each in a comment
- Show averageReadiness accepting a List<AirUnit> and a List<Unit>
- Show firstOrNull returning an AirUnit, not a Unit, when given a List<AirUnit>
- State the test you used to decide between T and ?
import java.util.*;
public class RegistryApi {
static class Unit { String name; double readiness;
Unit(String n, double r) { name = n; readiness = r; }
public String toString() { return name; } }
static class AirUnit extends Unit { AirUnit(String n, double r) { super(n, r); } }
// Choose the signature for each. Write your reason as a comment.
//
// 1. averageReadiness(...) reads every unit, returns a double
// 2. addStandardUnits(...) adds three new Unit objects to a list
// 3. countAll(...) returns the size of any list at all
// 4. moveAll(dest, src) moves everything from one list to another
// 5. firstOrNull(...) returns the first element, keeping its exact type
public static void main(String[] args) {
List<AirUnit> air = new ArrayList<>(List.of(new AirUnit("Falcon", 91)));
List<Unit> all = new ArrayList<>();
// TODO: call all five and show each accepts what it should
}
}
Hint 1
Hint 2
T rather than a wildcard: static <T> T firstOrNull(List<? extends T> list). The type appears twice, in the parameter and the return, and the two have to agree.Hint 3almost the answer
T first. If T appears only once in the whole signature, replace it with ?. If it appears twice or more, keep it. Number 3 is the clearest single-use case, so it is List<?>.After the credits
Phase IX is done, and it was the last groundwork before the biggest phase in the course.
You now have every piece the Collections Framework is built from:
| What Phase X uses | Where you got it |
|---|---|
List<E>, Map<K, V>, Set<E> |
generics, 9.3 |
addAll(Collection<? extends E> c) |
wildcards, this section |
A growing array underneath ArrayList |
4.2 and 9.2 |
hashCode and equals on your keys |
8.3 |
| Immutable keys that cannot go stale | 8.2 |
hash & (n - 1) to pick a bucket |
3.1 |
Map.Entry as a nested type |
7.4 |
| Interfaces first, classes second | 7.2 |
Comparable<T> is the one thing still owed. You met its shape in Section 9.2, when compareTo returned a negative number instead of a boolean. In Phase X it becomes the contract every sorting method in Java depends on, and it fails as quietly as equals does when you get it wrong.
Threads you opened in this section
- PECSCollections.copy(List<? super T>, List<? extends T>) is both halves in one line.Phase X. The Collections Framework
- PECSComparator and the function interfaces are full of it.Phase XI. Functional Java
Wildcards will return in Phase X. The Collections Framework