Skip to main content

CSE 331: Generics

lecture

Course motto: Your job is to deliver code that you know works.

A List<Integer> has been in this course's programs since Lecture 1, and Lecture 16 kept its durations in a Set<Duration>. No lecture has said yet how the angle brackets work, or who checks them. A type such as List is written once, for elements of a type its author never chose, and code that uses it writes the type in the brackets. What is written there is a claim about every element, and javac checks it. So this lecture asks two questions. How do we write such a type ourselves? And what happens when the brackets meet Lecture 14's subtypes? javac accepts a SaleProduct where a Product is expected. Does it accept a List<SaleProduct> where a List<Product> is expected? The answer to that question comes from a definition in Lecture 14. Most of the rest of the lecture is about writing types that say what we mean.

A queue of anything

Recall the queue for office hours, HelpQueue, and its first implementation, ListQueue, which keeps the students in one list. A student is named by a NetID, and a NetID is a String, so both types say String throughout: join takes a String, next returns one, and the list inside is a List<String>.

Now suppose a second client, a bakery, wants a queue of ticket numbers. The code of ListQueue would serve the bakery, since it calls no method that only a String has. But the types in ListQueue say String.

With what we know so far, there are two ways to reuse the code. The first is to copy the class and change String to Integer in the copy. Lecture 14 counted the cost of a copy: every later change has to be made in every copy, and nothing warns us about the copy we forgot. The second way is the one Java programs had to use before 2004.1 Write the class once, for Objects. Every class is a Java subtype of Object, so a queue of Objects accepts an element of any class. Here is that queue. It is ListQueue with Object written wherever String was:

ObjectQueue.java
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
private List<Object> queue = new ArrayList<Object>();

public int size() {
    return queue.size();
}

public void join(Object o) {
    if (queue.contains(o)) {
        throw new IllegalArgumentException(o + " is in the queue");
    }
    queue.add(o);
}

public Object next() {
    if (queue.isEmpty()) {
        throw new NoSuchElementException("the queue is empty");
    }
    return queue.remove(0);
}

public List<Object> waiting() {
    // A copy, so that a client cannot change the queue through it.
    return new ArrayList<Object>(queue);
}

And here is a client, an office hours program like Lecture 10's. Its queue is meant to hold NetIDs, and its first comment says so:

ObjectQueue.java
20
21
22
23
24
25
26
27
28
29
30
31
32
public static void main(String[] args) {
    // The queue for office hours. Every element is a NetID.
    ObjectQueue queue = new ObjectQueue();
    queue.join("ada");
    queue.join(1234567);
    queue.join("eve");

    // Later: help each student in turn.
    while (queue.size() > 0) {
        String netid = (String) queue.next();
        System.out.println("now helping " + netid);
    }
}

On line 29, next returns an Object, and main wants a String. The (String) in front of the call is a cast: it tells javac to treat the Object as a String. An Object might be a String, so javac accepts the cast. The check is made when the program runs: if the object is not a String, the cast throws a ClassCastException.

One of the three calls of join is a mistake. Find it. Then write down what you expect javac to say, and what you expect the program to print. Here is the run:

$ javac ObjectQueue.java

$ java ObjectQueue
now helping ada
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
    at ObjectQueue.main(ObjectQueue.java:29)

javac accepted every line. ada was helped. Then the program stopped at line 29, at the cast. The second element of the queue is the student number that line 24 joined, and a number is not a String. (Java wraps the int 1234567 in an object of the class Integer to pass it where an Object is expected. So the message says Integer.)

The mistake and the exception are on different lines. The mistake is on line 24, where a number joined a queue of NetIDs. The exception is thrown five lines later, on line 29, where the number came out. In a larger program, the call of join can be in one file and the cast in another. If the program read its NetIDs as input, it would fail only on a run on which somebody typed a number.

The comment on line 21 says Every element is a NetID. That is a claim about every element of the queue, on every run. Who checks it? Lecture 1 said that a tool can confirm a claim about every run, but only the one claim it was built to check, and its example of a tool was javac. Here is the claim that javac was built to check:

On every run, a variable refers only to objects of its declared type: objects whose class is that type or a Java subtype of it.

A parameter and a field are variables too, and the claim covers what a method returns in the same way. (A variable may also be null.) A language that keeps this claim is called type safe, and these notes call the claim type safety.2

javac confirms type safety one line at a time. Each place that takes an object expects a type: a variable that is stored into, a parameter, a method's return. javac accepts an expression there with no warning only if its type is the expected type or a Java subtype of it. (A number such as an int follows other rules: Java wraps it in an object where an object is expected, as on line 24.) So javac refuses a line that could break the claim, with two exceptions that this lecture shows. For those two, Java makes the check when the program runs. (The last section mentions a third kind of line, which javac accepts with a warning.)

In ObjectQueue.java, type safety holds, and it says nothing about NetIDs. The parameter of join is declared an Object, and so is each element of the list inside. A number is an Object, so javac accepts line 24. All that javac confirms about an element of this queue is that it is an Object.

The cast is the first of the two exceptions. On line 29, next returns an Object, and netid is declared a String. Without the cast, javac refuses the line: an Object need not be a String. The cast says: this Object is a String, trust me. javac cannot confirm that. So Java checks it when the program runs, and throws the exception before anything that is not a String reaches netid. Type safety still holds. But the check that an element is a String is now made one element at a time, and only on the runs that reach the cast. A check made one run at a time has the limit that Lecture 1 found in testing: it can refute a claim about every run and never confirm it.

So we want javac to check that every element is a String. For that, the claim has to be written where javac reads: in the queue's type.

A class with a type parameter

Java lets a class leave a type open, for each client to fill in. Here is the queue written that way, as an interface and a class. (Every program in this lecture is in the course's public repo, to download and run. A copy that javac refuses is kept there as a .txt file, and its opening comment says how to compile it.)

GenericQueue.java
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/**
 * A queue: the elements waiting, in the order they joined. An
 * element is in the queue at most once.
 */
interface Queue<E> {
    /** @return the number of elements waiting */
    int size();

    /**
     * @modifies this
     * @effects adds e at the back of the queue
     * @throws IllegalArgumentException if e is already in the queue,
     *     and leaves the queue unchanged
     */
    void join(E e);

    /**
     * @modifies this
     * @effects removes the element at the front of the queue
     * @return the element removed
     * @throws NoSuchElementException if the queue is empty
     */
    E next();

    /** @return the elements waiting, front first, in a new list */
    List<E> waiting();
}

/** A queue kept as one list of the elements in it. */
class ListQueue<E> implements Queue<E> {
    private List<E> queue = new ArrayList<E>();

    public int size() {
        return queue.size();
    }

    public void join(E e) {
        if (queue.contains(e)) {
            throw new IllegalArgumentException(e + " is in the queue");
        }
        queue.add(e);
    }

    public E next() {
        if (queue.isEmpty()) {
            throw new NoSuchElementException("the queue is empty");
        }
        return queue.remove(0);
    }

    public List<E> waiting() {
        // A copy, so that a client cannot change the queue through it.
        return new ArrayList<E>(queue);
    }
}

Start with line 43. The <E> after the name Queue declares a type parameter named E. Inside the interface, E stands for a type that we, the authors, do not know: the type of this queue's elements. So join takes an E, next returns an E, and waiting returns a List<E>.

A type parameter works like a method's parameter. A method's parameter stands for a value that the method's author does not know, and each call supplies the value. A type parameter stands for a type that the class's author does not know, and each use of the class supplies the type.

By convention a type parameter's name is one capital letter: E for an element, K and V for the key and the value of a map, and T for any type.3

Now line 68. ListQueue<E> declares a type parameter too, and then uses it three times before its first method: it implements Queue<E>, it keeps a List<E>, and it makes an ArrayList<E>. From there on, the class is Lecture 10's ListQueue with E written wherever String was, and with its parameter netid renamed e.

Definition: generic class, type parameter, type argument

A generic class or interface declares one or more type parameters in angle brackets after its name, as in class ListQueue<E>. Inside the class, a type parameter stands for a type that is chosen wherever the class is used.

Code that uses the class chooses by writing a type argument in the brackets, as in ListQueue<String>.

Java programmers call this feature generics. java.util.List is a generic interface like our Queue. It is declared as interface List<E>, in a file of Java like any other, and List<String> is that interface with String as its type argument.4 javac checks a List<String> by the same rules as our Queue<String>.

Here is the client again, with the queue's type filled in:

GenericQueue.java
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static void main(String[] args) {
    // The queue for office hours. Every element is a NetID.
    Queue<String> queue = new ListQueue<String>();
    queue.join("ada");
    queue.join("eve");

    // Later: help each student in turn.
    while (queue.size() > 0) {
        String netid = queue.next();
        System.out.println("now helping " + netid);
    }

    // The same class again, for a queue of ticket numbers.
    Queue<Integer> tickets = new ListQueue<Integer>();
    tickets.join(41);
    tickets.join(42);
    int ticket = tickets.next();
    System.out.println("now serving ticket " + ticket);
}

Line 20 declares queue a Queue<String>, so for this variable E is String. join takes a String. next returns a String, and line 26 stores it with no cast. Line 31 uses the same class for the bakery, with Integer as the type argument. Line 34 gets a number back, again with no cast. Here is the run:

$ javac GenericQueue.java

$ java GenericQueue
now helping ada
now helping eve
now serving ticket 41

Now make the mistake again. Suppose a copy of the program adds queue.join(1234567); as line 22, between the two students. What does javac say to it?

$ javac GenericQueue.java
GenericQueue.java:22: error: incompatible types: int cannot be converted to String
        queue.join(1234567);
                   ^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error

javac refuses the program, and it points at the line with the mistake. (The note in the message is javac's, and we can ignore it.) Since javac refuses the program, the program never runs, and no number reaches the queue.

The comment still says Every element is a NetID. But now the type of queue says that every element is a String, where javac reads it. For a Queue<String>, the parameter of join is a String, and next returns a String. So type safety, the claim that javac already confirms, now covers each element on its way into the queue and on its way out. javac checks every call of join on a Queue<String> against that claim. So no line of this program joins anything but a String, on any run. And javac knows that what next returns is a String, with nobody's word to take for it. That is why line 26 needs no cast.

No primitive types. The queue of tickets is a Queue<Integer>, not a Queue<int>, since javac refuses int as a type argument. A copy that writes int in both places on line 31 does not compile:

$ javac GenericQueue.java
GenericQueue.java:31: error: unexpected type
        Queue<int> tickets = new ListQueue<int>();
              ^
  required: reference
  found:    int
GenericQueue.java:31: error: unexpected type
        Queue<int> tickets = new ListQueue<int>();
                                           ^
  required: reference
  found:    int
2 errors

A type argument has to be a reference type: a class, an interface, an array type, or a type parameter such as E. int is none of these. So we write Integer, the class whose objects each hold one int. Java converts between the two as needed. Line 32 passes the int 41 where an Integer is expected, and line 34 stores the Integer that next returns in an int.

The queue's code stores an E and hands it back. What else may the code do with one?

What the code may do with an E

The code of ListQueue<E> does not know what E is: String for one client, Integer for the next. So javac lets the code do with an E only what is right for every type argument. Since every type argument is a reference type, an E that is not null refers to an object. The code may therefore call on an E the methods that every object has, the public methods of Object, and no others.

On lines 76 and 77, join does two things with e. It calls queue.contains(e), which compares e with each element by equals. And it writes e + " is in the queue", which calls toString on e. Both are methods of Object, so every E has them. (This is one more reason to keep Lecture 16's rule for equals. A queue of Durations decides whether a Duration is already in the queue by calling the equals that Duration wrote.)

Often the code needs more than Object has. Here is a first try at a method for Lecture 14's store, which finds the cheapest product in a list. It is meant to work on a list of any kind of product, and to hand back a product of the list's own element type. (The repo keeps this first try as CheapestUnbounded.txt.)

Cheapest.java
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
 * @requires list is not empty
 * @return a product in list whose price is the lowest in list
 */
public static <P> P cheapest(List<P> list) {
    P best = list.get(0);
    for (P p : list) {
        if (p.getPrice() < best.getPrice()) {
            best = p;
        }
    }
    return best;
}

One thing on line 38 is new. The <P> before the return type declares a type parameter that belongs to this one method. A method that declares a type parameter is a generic method. Its type parameter P may be used in the types of the method's parameters, in its return type, and in its body. Here cheapest takes a List<P> and returns a P.

Is the first try right? Decide which lines javac refuses:

$ javac Cheapest.java
Cheapest.java:41: error: cannot find symbol
            if (p.getPrice() < best.getPrice()) {
                 ^
  symbol:   method getPrice()
  location: variable p of type P
  where P is a type-variable:
    P extends Object declared in method <P>cheapest(List<P>)
Cheapest.java:41: error: cannot find symbol
            if (p.getPrice() < best.getPrice()) {
                                   ^
  symbol:   method getPrice()
  location: variable best of type P
  where P is a type-variable:
    P extends Object declared in method <P>cheapest(List<P>)
2 errors

Read the last line of the first message: P extends Object. The rule of this section holds for a method's type parameter too. The body may do with a P only what is right for every type, and Object has no getPrice. So javac refuses both calls of getPrice on line 41.

The method has to say that it is for products only. Here is line 38 as Cheapest.java has it:

Cheapest.java
38
public static <P extends Product> P cheapest(List<P> list) {

Here extends Product is a bound on P. The client may choose only Product, or a Java subtype of Product, for P. Since an object of any such type is a Product, the body may use a P as a Product. So line 41 may call getPrice, and javac accepts the method.

Definition: bound

A type parameter declared <P extends A> has the bound A. A type argument for P must be A or a Java subtype of A. The code in the scope of P may use a P as an A.

A type parameter declared with no bound, as in <E>, has the bound Object.

A bound works like a precondition, written about a type in place of a value. javac checks it at every call of a generic method, and wherever a type argument is written for a generic class. (The word is extends even when A is an interface.) Here are two calls of cheapest, on two lists with different element types:

Cheapest.java
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public static void main(String[] args) {
    // A cart, which may hold any product.
    List<Product> cart = new ArrayList<Product>();
    cart.add(new Product("skittles", 2.0));
    cart.add(new SaleProduct("m&ms", 2.0, 0.5));

    // The deals: sale products only.
    List<SaleProduct> deals = new ArrayList<SaleProduct>();
    deals.add(new SaleProduct("chips", 3.0, 0.5));
    deals.add(new SaleProduct("gum", 1.0, 0.5));

    Product p = cheapest(cart);
    System.out.println("cheapest(cart): " + p);
    SaleProduct s = cheapest(deals);
    System.out.println("cheapest(deals): " + s);
}

cart is Lecture 14's cart, a List<Product> that holds a Product and a SaleProduct. (The two classes are the ones in that lecture's ProductsFixed.java, whose toString calls getPrice, so a sale product prints at its sale price.) deals is new: a List<SaleProduct>, whose type says that it holds sale products only. Neither call of cheapest writes a type argument. javac works one out from the argument of each call:

  • cheapest(cart). cart is a List<Product>, so P is Product, and the call returns a Product.
  • cheapest(deals). deals is a List<SaleProduct>, so P is SaleProduct, and the call returns a SaleProduct. Line 30 stores it in a variable declared a SaleProduct, with no cast.

Here is the run:

$ javac Cheapest.java

$ java Cheapest
cheapest(cart): m&ms: $1.0
cheapest(deals): gum: $0.5

Why not write cheapest as Lecture 14 wrote total, to take a List<Product> and return a Product? Then the call on line 30 would hand back a Product, and line 30 would need a cast to store it as a SaleProduct. The next section finds a second reason.

A bound you will meet often is an interface of the JDK's, Comparable<T>, for types whose values have an order. It declares one method, compareTo, which takes a T. You may have written implements Comparable<Book> on a class in an earlier course: a Book can be compared with another Book. String implements Comparable<String>, and Integer implements Comparable<Integer>.5 A method that compares its elements can declare <T extends Comparable<T>>. The bound mentions T itself, which is allowed. Read it as: a T can be compared with another T. Exercise 2 writes such a method.

A list of sale products is not a list of products

Now for the question we started with. cheapest accepted the cart and the deals, since its P can be either element type. Lecture 14's total declares no type parameter. It takes a List<Product>. Here it is as that lecture wrote it, in CartsExact.txt, a copy of this lecture's Carts.java that is compiled under that name:

Carts.java
38
39
40
41
42
43
44
45
/** @return the sum of the prices of the products in cart */
public static double total(List<Product> cart) {
    double sum = 0.0;
    for (Product p : cart) {
        sum = sum + p.getPrice();
    }
    return sum;
}

In Lecture 14, main put a SaleProduct into a cart, and javac accepted the SaleProduct where a Product was expected. This time main makes the same cart and deals as Cheapest.java's main, and hands total each of the two lists, on lines 29 and 30:

Carts.java
29
30
System.out.println("total(cart): $" + total(cart));
System.out.println("total(deals): $" + total(deals));

Line 29 is Lecture 14's call. Line 30 hands total the deals, a List<SaleProduct>, where a List<Product> is expected. Every SaleProduct is a Product. So is a List<SaleProduct> accepted where a List<Product> is expected? Decide before you read on.

We do not have to guess. Lecture 14 defined a Java subtype by what the compiler does: B is a Java subtype of A if the compiler accepts a B where an A is expected. So to find out whether one type is a Java subtype of another, ask the compiler. Write a method that takes an A, hand it a B, and compile. CartsExact.txt is exactly that program:

$ javac Carts.java
Carts.java:30: error: incompatible types: List<SaleProduct> cannot be converted to List<Product>
        System.out.println("total(deals): $" + total(deals));
                                                     ^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error

javac refuses line 30. A List<SaleProduct> is not a Java subtype of List<Product>.

The is-a test said yes: in plain English, every list of sale products is a list of products. That test has misled us before, about a square and a rectangle. The way to settle the question is the same as it was there: ask what the supertype promises.

Lecture 14 compared two types method by method. A behavioral subtype needs, for each method, a precondition no stronger and a postcondition no weaker. That lecture also read part of each off a method's types: what a method returns belongs to its postcondition, and a wider parameter type is a weaker precondition. So let's compare two methods of the two lists by their types:6

  • get. A List<SaleProduct>'s get returns a SaleProduct, where a List<Product>'s returns a Product. Every SaleProduct is a Product, so the postcondition of get is no weaker.
  • add. In a List<Product>, add takes a Product, so a client may pass it any Product. In a List<SaleProduct>, add takes a SaleProduct, so a client may not pass it a plain Product. The parameter type is narrower, so the precondition of add is stronger.

A stronger precondition is what the definition rules out. So a List<SaleProduct> is not a behavioral subtype of List<Product>, and the rule from that lecture says not to declare it a Java subtype. Java follows that rule here: a List<SaleProduct> is not a Java subtype of List<Product>, as the refusal of line 30 showed.

But total never calls add. It only reads. So why does javac refuse a call that would have worked? javac checks a call against the type of the method's parameter. It does not look at the method's body. A client reasons the same way, from the specification alone. The type List<Product> lets the body of total add any Product to its cart. Suppose the store starts to charge ten cents for a bag. A later total could then begin with the line cart.add(new Product("bag", 0.1));. Its specification would gain @modifies cart, but its parameter's type would not change. Handed the deals, that line would put a plain Product into a list whose type says SaleProduct.

Here is the same mistake in a program with no method in the way. (The repo keeps it as ListCarts.txt, since javac refuses it.)

ListCarts.java
20
21
22
23
24
25
26
27
28
29
30
31
public static void main(String[] args) {
    // The deals: sale products only.
    List<SaleProduct> deals = new ArrayList<SaleProduct>();
    deals.add(new SaleProduct("chips", 3.0, 0.5));

    // A second name for the same list, declared a List<Product>.
    List<Product> all = deals;
    all.add(new Product("skittles", 2.0));

    SaleProduct second = deals.get(1);
    System.out.println("second deal: " + second);
}

Read the three steps as if javac accepted them all:

  • Line 26 gives the list of deals a second name, all, declared a List<Product>. No list is copied. all and deals are two names for one list.
  • Line 27 adds the skittles, a plain Product, through all. The type of all allows that. The skittles are now in the one list, so they are in deals.
  • Line 29 reads the second element through deals, whose type says the element is a SaleProduct. The element is the skittles, and the skittles are not a SaleProduct.

Lines 27 and 29 each do what the type of their variable allows. So the line to refuse is line 26, and javac refuses it:

$ javac ListCarts.java
ListCarts.java:26: error: incompatible types: List<SaleProduct> cannot be converted to List<Product>
        List<Product> all = deals;
                            ^
1 error

Rule: the type arguments must be the same

For a generic class or interface C, a C<B> is accepted where a C<A> is expected only if A and B are the same type. It is not enough for B to be a Java subtype of A.

So a List<SaleProduct> is not accepted where a List<Product> is expected.

Programmers call this property invariance: Java's generic types are invariant in their type arguments.

The rule is about the type arguments inside the brackets. The class or interface outside them still follows Lecture 14's chain of extends and implements. ArrayList<E> is declared to implement List<E>, so an ArrayList<Product> is a Java subtype of List<Product>. Cheapest.java and Carts.java each store an ArrayList<Product> in a variable declared a List<Product>.7

Arrays took the other answer

Java has had arrays since its first version, years before it had type parameters. For arrays, Java gave the other answer: SaleProduct[] is a Java subtype of Product[].8 So the same three steps, written with arrays, compile:

ArrayCarts.java
15
16
17
18
19
20
21
22
23
24
25
26
public static void main(String[] args) {
    // The deals: sale products only.
    SaleProduct[] deals = new SaleProduct[2];
    deals[0] = new SaleProduct("chips", 3.0, 0.5);

    // A second name for the same array, declared a Product[].
    Product[] all = deals;
    all[1] = new Product("skittles", 2.0);

    SaleProduct second = deals[1];
    System.out.println("second deal: " + second);
}

javac accepts line 21 this time. So what happens to the skittles on line 22? Predict the run:

$ javac ArrayCarts.java

$ java ArrayCarts
Exception in thread "main" java.lang.ArrayStoreException: Product
    at ArrayCarts.main(ArrayCarts.java:22)

The program stopped at line 22, the store, with an ArrayStoreException. Each array keeps, while the program runs, the class it was made to hold. Each time a program stores an object in an array, Java checks the object against that class, when the program runs. The array that deals and all name was made to hold sale products, and the skittles are not one.

So the skittles never reached deals. But compare when the mistake is found in each case. With lists, javac refused the second name, so the program never ran. With arrays, javac accepted every line, and the program failed on the run that made the bad store. As far as javac checks, a Product[] takes any Product in a store, and a SaleProduct[] cannot take a plain Product. So by its types, SaleProduct[] is a Java subtype of Product[] that is not a behavioral subtype: here Java itself breaks Lecture 14's rule. To catch such a store, Java checks every store into an array of objects, when the program runs.

That check is the second of the two exceptions that A queue of anything counted. javac accepts line 22, which could break type safety, and Java makes the check when the program runs. As with the cast, type safety still holds: the skittles never reach the array. And as with the cast, the price is a program that can fail on some run.

A wildcard asks for less

Back to total(deals). javac was right to refuse it: a method that takes a List<Product> may add to the list. But our total never adds. Its parameter's type, List<Product>, allows an add that its body never makes.

Lecture 2 said of its specifications A and D that A asks more of its client than D does, which made D the stronger of the two. Our total is in A's place: it asks its client for a list that can be added to. So let total ask for less. Here it is with one change, in the type of its parameter:

Carts.java
38
39
40
41
42
43
44
45
/** @return the sum of the prices of the products in cart */
public static double total(List<? extends Product> cart) {
    double sum = 0.0;
    for (Product p : cart) {
        sum = sum + p.getPrice();
    }
    return sum;
}

The ? is a wildcard: it stands for a type that the method does not name. Read List<? extends Product> as a list of some one type, which is Product or a Java subtype of Product. A List<Product> is such a list. So is a List<SaleProduct>.

The rule of the last section is unchanged. Its A and B are types written out, such as Product and SaleProduct. A wildcard is not one type: ? extends Product stands for any one of many. So a List<SaleProduct> is still not a List<Product>. But it is a List<? extends Product>, which is a different type. In a List<Product>, an element may be a SaleProduct, but the type argument is known: it is Product, so add takes any Product. In a List<? extends Product>, the type argument itself is not known.

Now javac accepts both calls, and each prints its total:

$ javac Carts.java

$ java Carts
total(cart): $3.0
total(deals): $2.0
deals: [chips: $1.5, gum: $0.5, mints: $0.5]
cart: [skittles: $2.0, m&ms: $1.0, mints: $0.5]

(The last two lines come from a second method, below.)

With the wildcard, the body of total may no longer add to cart. A copy in which total adds the bag for ten cents, as line 40, does not compile:

$ javac Carts.java
Carts.java:40: error: incompatible types: Product cannot be converted to CAP#1
        cart.add(new Product("bag", 0.1));
                 ^
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Product from capture of ? extends Product
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error

CAP#1 is javac's name for the type that the ? stands for. On line 40, cart is a list of CAP#1s. All javac knows about CAP#1 is that it is Product or a Java subtype of Product. Since CAP#1 might be SaleProduct, or a class not yet written, javac refuses the call.

The body may still read. Whatever type the ? stands for, an element of the list is a Product. So the loop on line 41 may store each element in a variable declared a Product.

The other direction. A method that only adds can ask for less in the other direction. Here is one, which puts a pack of mints, on sale, at the end of a list:

Carts.java
47
48
49
50
51
52
53
/**
 * @modifies list
 * @effects adds a pack of mints, on sale, at the end of list
 */
public static void addMints(List<? super SaleProduct> list) {
    list.add(new SaleProduct("mints", 1.0, 0.5));
}

Read List<? super SaleProduct> as a list of some one type, which is SaleProduct or a Java supertype of SaleProduct. That could be a List<SaleProduct>, a List<Product>, or a List<Object>. A SaleProduct may be added to each of the three. So javac accepts line 52, and main calls addMints on both of its lists:

Carts.java
32
33
34
35
addMints(deals);
addMints(cart);
System.out.println("deals: " + deals);
System.out.println("cart: " + cart);

The last two lines of the run above show the mints at the end of each list.

Through list, javac does not know the type of an element that comes out. A copy in which addMints reads the first element into a variable declared a SaleProduct, as line 53, does not compile:

$ javac Carts.java
Carts.java:53: error: incompatible types: CAP#1 cannot be converted to SaleProduct
        SaleProduct first = list.get(0);
                                    ^
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object super: SaleProduct from capture of ? super SaleProduct
1 error

Since the list might be a List<Object>, all javac knows about an element is that it is an Object.

Being the compiler. How do we decide whether javac accepts a call through a wildcard? javac does not know which list the variable refers to. So it accepts a call only if the call is right for every list that the variable may refer to, a list of a class not yet written included. To see that javac refuses a call, find one list that the call would be wrong for. Here are three elements and three lists, the parameters of one method:

Table.java
20
21
22
23
static void foo(Object o, Product p, SaleProduct s,
        List<Product> cart,
        List<? extends Product> from,
        List<? super SaleProduct> to) {

Before you read the table, decide for each call of add and get, through each of the three lists, whether javac accepts it. The table gives javac's answers:

the call cart from to
add(o) refused refused refused
add(p) accepted refused refused
add(s) accepted refused accepted
o = get(0) accepted accepted accepted
p = get(0) accepted accepted refused
s = get(0) refused refused refused

Two of its cells have split the room in past quarters of this course:

  • from.add(s) is refused. A SaleProduct is a Product, so the call looks safe. But from is a list of some one type, and javac does not know which. Say someone later writes ClearanceProduct, a subclass of SaleProduct. Then from may be a List<ClearanceProduct>, and a plain SaleProduct does not belong in that list. Lecture 14 said of SaleProduct that it could have been written a year after total, by someone else, and so could ClearanceProduct. So through from, javac accepts no add of any object, whatever its class.
  • to.add(o) is refused. Since Object is a supertype of SaleProduct, a reader may think the list could hold an Object. But to may be a List<SaleProduct>, and o may refer to an object that does not belong in that list. Neither does a plain Product, so to.add(p) is refused too.

One value can be added through all three lists: null, which javac accepts in place of an object of any type, CAP#1 included. So from.add(null) compiles.

Rule: get through extends, add through super

A List<? extends A> is a list of some one type that is A or a Java subtype of A. Its get returns an A, and its add accepts nothing but null.

A List<? super B> is a list of some one type that is B or a Java supertype of B. Its add accepts a B, and its get returns an Object.

So a method that only reads from a list takes a List<? extends A>, a method that only adds to one takes a List<? super B>, and a method that does both takes a List<A>.

Here is the rule as a picture of which list javac accepts where:

In the middle, three lists: List of Object above List of
Product above List of SaleProduct, with a crossed arrow from each
to the one above it. On the left, List of ? extends Product, with
an arrow to it from List of Product and from List of SaleProduct.
On the right, List of ? super SaleProduct, with an arrow to it
from each of the three.

Three lists, and the two wildcard types that accept them. Each arrow between two of the three lists is crossed, since their type arguments differ.

Naftalin and Wadler call the rule the Get and Put Principle. Joshua Bloch's mnemonic for it is PECS: producer extends, consumer super.9 (The producer or consumer is the list: a list that total reads from produces Products for total.) The JDK uses the rule in its own signatures. List<E>'s method addAll takes a Collection<? extends E>, so a List<Product> can take in all of a List<SaleProduct> in one call.11

A wildcard with no bound. A ? may stand alone. Read List<?> as a list of some one type, any type at all. It means the same as List<? extends Object>, so its get returns an Object, and its add accepts nothing but null:10

Table.java
41
42
43
44
static void bare(Object o, List<?> any) {
    o = any.get(0);
    any.add(null);
}

A wildcard or a name? total could have named its type, as cheapest did: <P extends Product>, with a parameter of type List<P>. That signature accepts the cart and the deals too. The wildcard is for a type that the method never needs to name again. cheapest needs the name, to say that the product it returns has the same type as the elements of its list.

When the program runs

Lecture 16 said this of a HashSet, in passing: inside the set, an element has the type Object, whatever the set's declared element type. In the source, that is the rule of What the code may do with an E: the code of HashSet<E> may use an E only as an Object. The sentence is also true of the running program, for a reason this section gives.

javac checks the type arguments, and then it leaves them out of the code that runs. The compiled ListQueue is one class, for every type argument, and inside it an element is an Object, as it was in ObjectQueue. Where a client needs a String back, javac puts in the cast that we wrote by hand at the start of this lecture. This is called type erasure.12 The difference from ObjectQueue is that javac checked every call of join first. So in this lecture's programs, the casts that javac puts in do not fail. (A program can get around the check: with a type written with no type argument at all, such as a plain List, or with a cast to a type such as List<String>. javac calls such a line unchecked, and after one, a cast that javac put in can fail.)

Here is a program that looks at two queues while it runs:

Erasure.java
20
21
22
23
24
25
26
27
28
public static void main(String[] args) {
    Queue<String> names = new ListQueue<String>();
    Queue<Integer> tickets = new ListQueue<Integer>();
    System.out.println("names:   " + names.getClass());
    System.out.println("tickets: " + tickets.getClass());

    Object o = tickets;
    System.out.println("Queue<?>: " + (o instanceof Queue<?>));
}

getClass returns the class of an object. Line 27 asks whether o is a Queue<?>, a queue of some type. Here is the run:

$ javac Erasure.java

$ java Erasure
names:   class ListQueue
tickets: class ListQueue
Queue<?>: true

Both objects have the class ListQueue, with no type argument. When the program runs, nothing about either object says String or Integer.

Java made this choice in 2004, to fit type parameters into a language whose programs had been compiled and shipped for years. A class compiled before type parameters and a class compiled after can use the same ArrayList, since there is only one. Since the type arguments are gone, a running program cannot ask an object for its type argument. You will meet this in two places:

  • instanceof cannot check a type argument when the program runs. A copy whose line 27 asks whether o is a Queue<String> does not compile:

    $ javac Erasure.java
    Erasure.java:27: error: Object cannot be safely cast to Queue<String>
            System.out.println("Queue<String>: " + (o instanceof Queue<String>));
                                                    ^
    1 error
    

    An instanceof asks whether o could be cast to the type, so javac reads the question as a cast from Object to Queue<String>. Nothing could check such a cast when the program runs, and the message says so. The question that can be answered is the one line 27 asks: is o a queue of some type? That matters for the equals of Lecture 16, which opens with instanceof. In a generic class, that instanceof asks for a Queue<?>, as exercise 5 does.

  • A generic class cannot write new E[10]. javac refuses it, with the message generic array creation. Each array keeps the class it was made to hold, for Java to check each store against, as it checked the store in ArrayCarts.java. When the program runs there is no E for an array to keep. ListQueue writes no array: it keeps its elements in a List<E>.

So a type argument is checked when javac compiles the program, and it is gone from the code that runs. What is left of it then is the casts that javac put in.

Exercises

Each answer is folded under its question. Write yours first.

1. Lecture 10's second implementation of the help queue, LinkedQueue, keeps its students in a chain of nodes. Here is how the class opens:

LinkedQueue.java
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/** A help queue kept as a chain of nodes, one node per student. */
public class LinkedQueue implements HelpQueue {
    /** One student waiting, and the node of the student after them. */
    private static class Node {
        String netid;
        Node next;  // null for the last node in the chain

        Node(String netid) {
            this.netid = netid;
        }
    }

    // The first and last nodes of the chain, both null when nobody
    // is waiting.
    private Node front;
    private Node back;

Give LinkedQueue a type parameter, so that it implements this lecture's Queue<E>. Which of these lines change, and how? One class here needs more than a String turned into an E. Which one, and why?

Answer
  • Every String becomes E, and Node gets a type parameter too. Here are the same lines of the generic class. Its field netid is renamed element, since an element need not be a student:

    GenericLinked.java
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    /** A queue kept as a chain of nodes, one node per element. */
    class LinkedQueue<E> implements Queue<E> {
        /** One element waiting, and the node of the element after it. */
        private static class Node<E> {
            E element;
            Node<E> next;  // null for the last node in the chain
    
            Node(E element) {
                this.element = element;
            }
        }
    
        // The first and last nodes of the chain, both null when nothing
        // is waiting.
        private Node<E> front;
        private Node<E> back;
    
  • Node is the class that needs more. A node holds an element, so a node's type depends on the element type as the queue's does. Node is declared static, which means that a node belongs to no one queue. So the code of Node cannot use the queue's E. If it tries, with E element; in a Node that declares no type parameter, javac refuses the field: it says non-static type variable E cannot be referenced from a static context. Node declares a type parameter itself, on line 62. Every use of Node then writes a type argument: the fields front and back are each a Node<E>, and join makes a new Node<E>(e).

  • What does not change. size still returns an int, since it counts the elements, whatever their type. Only a String that stood for an element becomes an E.

  • The run. main joins ada and eve to a LinkedQueue<String>, prints waiting(), and calls next once. Then it joins 41 and 42 to a LinkedQueue<Integer>:

    $ javac GenericLinked.java
    
    $ java GenericLinked
    waiting: [ada, eve]
    next: ada
    tickets: [41, 42]
    

2. Write a method max that returns the largest element of a list, as compareTo orders the elements. a.compareTo(b) returns a number greater than 0 when a is greater than b. The method should work on a List<String> and on a List<Integer>. What is its type parameter's bound? Then say whether javac accepts a call of max on a List<Product>. (Product is declared with no implements.)

Answer
  • The bound is Comparable<T>. The body calls compareTo on a T, and passes it another T. Comparable<T> is the interface that declares compareTo with a parameter of type T, so that is what a T has to be:

    Max.java
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    /**
     * @requires list is not empty
     * @return an element of list that compareTo puts no other
     *     element of list above
     */
    public static <T extends Comparable<T>> T max(List<T> list) {
        T best = list.get(0);
        for (T t : list) {
            if (t.compareTo(best) > 0) {
                best = t;
            }
        }
        return best;
    }
    

    The body is cheapest's, with compareTo in place of the two calls of getPrice.

  • The run, on the list eve, ada, bob and the list 41, 7, 42:

    $ javac Max.java
    
    $ java Max
    max(names): eve
    max(tickets): 42
    
  • javac refuses max on a List<Product>. For that call T would be Product, and Product does not implement Comparable<Product>. The bound is the method's precondition on its type argument, and this call does not meet it. If Product were declared implements Comparable<Product>, with a compareTo that compares prices, the call would compile.

  • The JDK's own max asks for less. Collections.max bounds its T by Comparable<? super T>: a T can be compared by a compareTo written for T or for a supertype of T. So it also accepts a list of a class that inherits its compareTo, as SaleProduct would inherit Product's. With our bound, javac would refuse max on a List<SaleProduct>, since a SaleProduct would be a Comparable<Product> and not a Comparable<SaleProduct>.13

3. Ask the compiler, on paper. Here are eight variables:

Assignments.java
19
20
21
22
23
static void accepted(List<Product> cart, List<SaleProduct> deals,
        ArrayList<Product> made, List<Object> things,
        List<? extends Product> from,
        List<? super SaleProduct> to,
        Product[] products, SaleProduct[] sales) {

For each of these ten assignments, say whether javac accepts it. An assignment x = y is accepted when the type of y is a Java subtype of the type of x.

  1. cart = made;
  2. cart = deals;
  3. things = cart;
  4. from = deals;
  5. from = things;
  6. to = things;
  7. to = deals;
  8. products = sales;
  9. sales = products;
  10. deals = from;
Answer
  • javac accepts five of the ten:

    the assignment accepted? why
    1 cart = made yes ArrayList implements List
    2 cart = deals no Product and SaleProduct differ
    3 things = cart no Object and Product differ
    4 from = deals yes SaleProduct extends Product
    5 from = things no Object is not a subtype of Product
    6 to = things yes SaleProduct is a subtype of Object
    7 to = deals yes SaleProduct is SaleProduct
    8 products = sales yes arrays took the other answer
    9 sales = products no a Product is not a SaleProduct
    10 deals = from no from may be a List<Product>
  • Assignments 2 and 3 are the lecture's rule. A List<SaleProduct> is not a List<Product>, and by the same rule a List<Product> is not a List<Object>. The second surprises people who have just accepted the first. But the rule makes no exception for Object.

  • Assignment 10 runs the wildcard backward. A List<SaleProduct> is accepted where a List<? extends Product> is expected, as in assignment 4. The other way around, javac knows only that from is a list of some subtype of Product, and that is not enough to call it a list of sale products.

4. Be the compiler once more, this time on the JDK's own types. Integer and Double are each a Java subtype of Number, a class of the JDK's, and neither is a subtype of the other. Here are seven variables:

Numbers.java
18
19
20
21
static void accepted(Object o, Number n, Integer i, Double d,
        List<Number> nums,
        List<? extends Number> from,
        List<? super Integer> to) {

For each of these twelve lines, say whether javac accepts it. For each one it refuses, name a list that the line would be wrong for.

  1. nums.add(i);
  2. nums.add(d);
  3. nums.add(o);
  4. n = from.get(0);
  5. i = from.get(0);
  6. from.add(i);
  7. from.add(null);
  8. to.add(i);
  9. to.add(n);
  10. to.add(d);
  11. o = to.get(0);
  12. n = to.get(0);
Answer
  • javac accepts six of the twelve:

    the line accepted? a list it would be wrong for
    1 nums.add(i) yes
    2 nums.add(d) yes
    3 nums.add(o) no nums itself, a List<Number>
    4 n = from.get(0) yes
    5 i = from.get(0) no a List<Double>
    6 from.add(i) no a List<Double>
    7 from.add(null) yes
    8 to.add(i) yes
    9 to.add(n) no a List<Integer>
    10 to.add(d) no a List<Integer>
    11 o = to.get(0) yes
    12 n = to.get(0) no a List<Object>
  • Lines 1 and 2 need no wildcard. nums is a List<Number>, so its add takes a Number, and an Integer or a Double is accepted where a Number is expected. That is Lecture 14's subtyping, applied to one element. The rule of this lecture is about a list passed where a list is expected, and lines 1 and 2 pass an element.

  • Line 12 is refused. Everything that to.add accepts is an Integer, so a reader may expect a Number to come out. But to may be a List<Object> that already held a String before we were handed it.

5. Give ListQueue<E> the equals and the hashCode of Lecture 16: two queues are equal when the same elements are waiting in each, in the same order. What does the instanceof in equals ask for? Then predict the three lines this main prints:

EqualQueue.java
17
18
19
20
21
22
23
24
25
26
27
28
29
public static void main(String[] args) {
    Queue<String> names = new ListQueue<String>();
    Queue<Integer> tickets = new ListQueue<Integer>();
    System.out.println("both empty: " + names.equals(tickets));

    names.join("ada");
    tickets.join(41);
    System.out.println("ada and 41: " + names.equals(tickets));

    Queue<String> more = new ListQueue<String>();
    more.join("ada");
    System.out.println("ada and ada: " + names.equals(more));
}
Answer
  • The two methods are Lecture 16's, with one change:

    EqualQueue.java
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    @Override
    public boolean equals(Object o) {
        if (o instanceof Queue<?> q) {
            return waiting().equals(q.waiting());
        }
        return false;
    }
    
    @Override
    public int hashCode() {
        return waiting().hashCode();
    }
    

    As in that lecture, the interface Queue<E> declares both methods too, to say what every implementation must do.

  • The instanceof asks for a Queue<?>, a queue of some type. It cannot ask for a Queue<E>: when the program runs, o carries no type argument to check. So q is a queue of elements of a type that equals does not know, and q.waiting() is a list of them. equals needs nothing more about them, since List's equals compares the two lists element by element, by calling each element's own equals.

  • The run:

    $ javac EqualQueue.java
    
    $ java EqualQueue
    both empty: true
    ada and 41: false
    ada and ada: true
    
  • An empty queue of Strings equals an empty queue of Integers. Both stand for the queue with nobody in it, and nothing in either object says String or Integer when the program runs. Once each has an element, List's equals compares the two elements by calling equals on one of them, and the String "ada" does not equal the Integer 41.

6. Here is a method that adds the elements of one list at the end of another. It declares a type parameter T, and each of its parameters is a List<T>:

Copy.java
32
33
34
35
36
37
38
39
40
41
/**
 * @requires src and dst are not the same list
 * @modifies dst
 * @effects adds the elements of src, in order, at the end of dst
 */
static <T> void copy(List<T> src, List<T> dst) {
    for (T t : src) {
        dst.add(t);
    }
}

main makes the cart and the deals of the lecture, and then calls copy(deals, cart) on line 28, to add the deals to the cart. javac refuses that call. Why? Then change the types of copy's two parameters, and nothing else, so that javac accepts the call. Is there more than one way? (The repo keeps this version as CopyExact.txt, since javac refuses it.)

Answer
  • No one T fits both lists. Both parameters are a List<T>, with the same T. deals is a List<SaleProduct>, so T would have to be SaleProduct. cart is a List<Product>, so T would have to be Product. By the lecture's rule, neither list is accepted as the other's type, and javac says so:

    $ javac Copy.java
    Copy.java:28: error: method copy in class Copy cannot be applied to given types;
            copy(deals, cart);
            ^
      required: List<T>,List<T>
      found:    List<SaleProduct>,List<Product>
      reason: inference variable T has incompatible equality constraints Product,SaleProduct
      where T is a type-variable:
        T extends Object declared in method <T>copy(List<T>,List<T>)
    1 error
    
  • copy only reads from src, and only adds to dst. So the rule box gives each parameter a wildcard: get through extends, add through super.

    Copy.java
    37
    static <T> void copy(List<? extends T> src, List<? super T> dst) {
    

    Now a T fits both lists. Product does. The deals are a list of a subtype of Product. The cart is a list of Product itself.

  • The run. The two deals are at the end of the cart:

    $ javac Copy.java
    
    $ java Copy
    cart: [skittles: $2.0, m&ms: $1.0, chips: $1.5, gum: $0.5]
    
  • Either wildcard alone is enough for this call. With src a List<? extends T> and dst a List<T>, T is Product. With src a List<T> and dst a List<? super T>, T is SaleProduct. The JDK's own Collections.copy writes both wildcards.14

7. Exercise 6 ended with four ways to declare copy. Each declares <T>, and all four have the same body and the same Javadoc. They differ only in the types of the two parameters:

src dst
A List<T> List<T>
B List<? extends T> List<T>
C List<T> List<? super T>
D List<? extends T> List<? super T>

A client has four lists: cart and more, each a List<Product>; deals, a List<SaleProduct>; and things, a List<Object>. For each of these four calls, say under which of the four declarations javac accepts it:

  1. copy(cart, more);
  2. copy(deals, cart);
  3. copy(deals, things);
  4. copy(cart, deals);

Then compare the four declarations as Lecture 2 compared specifications. Which one asks the most of its client?

Answer
  • javac's answers, with yes for a call it accepts:

    the call A B C D
    1 copy(cart, more) yes yes yes yes
    2 copy(deals, cart) no yes yes yes
    3 copy(deals, things) no yes yes yes
    4 copy(cart, deals) no no no no
  • A asks the most of its client. Under A, both lists must have the same type argument, so javac refuses calls 2 and 3. Every call that A accepts, B, C, and D accept too, with the same T: a List<T> is accepted where a List<? extends T> or a List<? super T> is expected. So each of B, C, and D accepts every call that A does, and more. The precondition of each is weaker than A's. The postcondition of each is the same as A's. By Lecture 2's rule, each of B, C, and D is a stronger specification than A.

  • B, C, and D agree on these four calls. D is the one that the rule box gives. Its types say, of each parameter, the one thing the body does with it: copy reads from src and adds to dst.

  • Call 4 is refused under all four, and javac is right. The call would add the cart's products to deals, and a cart may hold a plain Product. Under D, T would have to be a supertype of Product and a subtype of SaleProduct, and no type is both.

Further reading

  • The tutorial that introduced type parameters to Java programmers. Gilad Bracha, "Generics in the Java Programming Language" (2004), 23 pages. Its sections 1 to 5 cover casts, generic classes, subtyping, wildcards, and generic methods, in that order. Its sections 6 and 7 go on to what this lecture left out or only touched: a class compiled before type parameters used beside one compiled after, and the fine print of erasure.
  • Advice on using them well. Joshua Bloch, Effective Java, third edition (2018), chapter 5, "Generics", items 26 to 33. Item 28, "Prefer lists to arrays", sets the two answers of the sections A list of sale products is not a list of products and Arrays took the other answer side by side. Item 31, "Use bounded wildcards to increase API flexibility", is the rule of the section A wildcard asks for less, with a stack whose pushAll takes ? extends E and whose popAll takes ? super E.
  • The rule, argued from examples. Maurice Naftalin and Philip Wadler, Java Generics and Collections (2006), chapter 2, "Subtyping and Wildcards". Section 2.4 is the Get and Put Principle, and section 2.5 compares arrays with lists.
  • Oracle's lesson. Oracle, The Java Tutorials, "Generics", written for JDK 8. Its last page, "Questions and Exercises: Generics", has short questions, and links to their answers.

  1. The angle brackets came to Java in version 5, released in 2004. Gilad Bracha, "Generics in the Java Programming Language" (2004), is a tutorial written to introduce them. Its section 1, "Introduction", shows a cast of the kind this section's client writes, (Integer) myIntList.iterator().next(), and says that such a cast introduces "the possibility of a run time error". ↩

  2. The Java Language Specification, Java SE 25 edition, section 4.12.2, "Variables of Reference Type": "A variable of a class type T can hold a null reference or a reference to an instance of class T or of any class that is a subclass of T." Section 5.1.6.3 says of a cast such as the one on line 29 that it needs "a validity check at run time". Section 4.12.2 goes on to say that the claim is not guaranteed for a type with a type argument, once a program has a line that javac warns about as unchecked. ↩

  3. Oracle, The Java Tutorials, "Generic Types", under "Type Parameter Naming Conventions". ↩

  4. The Java Language Specification, Java SE 25 edition, section 9.1.2, "Generic Interfaces and Type Parameters" (section 8.1.2 says the same of classes), and section 4.5, "Parameterized Types", which is its name for a type such as List<String>. The declaration of List is in the documentation of java.util.List in Java 25. ↩

  5. The documentation of java.lang.Comparable, of String, and of Integer in Java 25. The last two list the interfaces each class implements. ↩

  6. The comparison reads only the types. The documentation of java.util.List in Java 25 lets a list's add refuse an element with an exception, for one, "if the class of the specified element prevents it from being added to this list". ↩

  7. The Java Language Specification, Java SE 25 edition, section 4.10.2, "Subtyping among Class and Interface Types", and section 4.5.1, "Type Arguments of Parameterized Types". ↩

  8. The Java Language Specification, Java SE 25 edition, section 4.10.3, "Subtyping among Array Types", and section 10.5, "Array Store Exception". ↩

  9. Maurice Naftalin and Philip Wadler, Java Generics and Collections (2006), section 2.4, "The Get and Put Principle". Joshua Bloch, Effective Java, third edition (2018), item 31, "Use bounded wildcards to increase API flexibility". ↩

  10. The Java Language Specification, Java SE 25 edition, section 4.5.1, "Type Arguments of Parameterized Types": "The wildcard ? extends Object is equivalent to the unbounded wildcard ?." ↩

  11. The documentation of List.addAll in Java 25. ↩

  12. The Java Language Specification, Java SE 25 edition, section 4.6, "Type Erasure", and section 4.7, "Reifiable Types", which gives the reason for the choice: "compatibility with existing code". ↩

  13. The documentation of Collections.max in Java 25. Its type parameter is declared <T extends Object & Comparable<? super T>>, and its parameter is a Collection<? extends T>. ↩

  14. The documentation of Collections.copy in Java 25. It takes the destination first: its parameters are a List<? super T> and then a List<? extends T>. ↩