Layers of Logic

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.

Going deeper18 min read4 exercises
01

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.

02

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;      // ERROR
incompatible 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

  1. List<Animal> animals = dogs;Both variables now point at the same list. That is Section 6.3: assignment copies the reference.
  2. animals.add(new Cat());Completely legal. animals is a list of Animals, and a Cat is an Animal.
  3. 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.

03

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.0

List<? 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());       // ERROR

The 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>());    // works

List<? 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);         // ERROR

The list might really hold Object. The only thing everything is, is an Object.

04

Under the hood

Going deeper

The three shapes side by side

DeclarationWhat 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 allread Object. Cannot write.
Notice the pattern: whichever direction you open up, you lose the other operation.

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> and List<Animal> are unrelated types.
  • Covariant: the relationship follows the elements. List<? extends Animal> accepts a List<Dog>.
  • Contravariant: the relationship runs backwards. List<? super Dog> accepts a List<Animal>.

PECS

There is a rule that tells you which to use, and it is four letters.

PECS: Producer extends, Consumer super.

Ask one question about the parameter: is it giving me values, or am I giving it values?

The parameter is aSo
Producerit gives you values. You read.? extends
Consumerit takes values. You write.? super
Bothyou read and writeno wildcard. Use the exact type.
Neitheryou only need any listList<?>

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>();     // fine

List<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.

05

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.

06

Check yourself

Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.

  1. 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;. Now animals and dogs point at the same list. Then somebody writes animals.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> and List<Animal> have no relationship at all, even though Dog and Animal do.

  2. 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 a List<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#1 is the compiler's name for "the one specific type this really is, which I cannot see".

    So ? extends gives you a list you can read from. That is all, and it is often all you need.

  3. 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 is extends. The destination consumes, so it is super.

    If you neither read nor write, and only want any list at all, use plain List<?>.

07

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

The Layers of Logic VS Code extension runs the checks for exercises marked checked. For a manual exercise, run the program and compare its behaviour with the stated requirements and sample output.
A

Break It If Java Let You

Real work·20 min·20 points

ex-9-4-a

Java 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
Invariant.java
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
A single 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
Trace it: if the assignment were allowed, both variables point at one list. Adding a Cat through the Animal view is legal. Then reading a Dog out of the Dog view crashes.
Hint 3almost the answer
Invariant means no relationship at all. Dog and Animal are related. List<Dog> and List<Animal> are two unrelated types, and Java keeps them that way on purpose.
What this is really testing

Whether you can explain the invariance rule by showing what it prevents. The rule looks arbitrary until you write the three lines it stops.

B

Read With extends

Real work·25 min·25 points

ex-9-4-b

Write 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
Reading.java
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
Adding fails with a message mentioning 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.
What this is really testing

Whether you can write a method that accepts any list of a family. Without a wildcard the method compiles and nobody can call it.

C

Write With super

Real work·25 min·25 points

ex-9-4-c

The 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
Writing.java
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
Reading from ? 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
The signature is static <T> void copy(List<? super T> dest, List<? extends T> src). The source produces values, so extends. The destination consumes them, so super.
What this is really testing

Whether you can use the other half of PECS. super is the one people skip, and Collections.copy uses both halves in a single signature.

D

Pick the Right Signature

Hard·30 min·30 points·The Registry

ex-9-4-d

Five 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 ?
RegistryApi.java
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
Ask PECS for each one. Does the parameter give me values, or do I give it values? 1 and 3 read. 2 writes. 4 does both, one each way. 5 reads, and the caller needs the exact type back.
Hint 2
Number 5 is the one that needs 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
The test: write it with 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<?>.
What this is really testing

Whether you can choose between T, ? extends, ? super and plain ? for a real API. Every one of the five methods here has a right answer, and a reason.

08

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

Wildcards will return in Phase X. The Collections Framework