Learn Java

Lesson 33Phase 4: Collections and algorithms

Generics: one class for many types

Writing a class or method once and using it with any type, safely, and what type erasure means.

By Updated 50 min

  • type parameter
  • generic class
  • generic method
  • diamond
  • bounded type
  • wildcard

Since your first ArrayList you have written ArrayList<String> and List<Integer>, with the angle brackets as part of the recipe: they say what the list holds. This lesson explains them. You will write your own classes and methods that take a type in angle brackets, see how that turns a crash at run time into an error the compiler reports first, and find out what Java keeps of those types once the program runs.

By the end of this lesson
  • Explain the problem generics solve: a class that stores Object needs casts, and a wrong value only fails at run time.
  • Write a generic class with one or two type parameters, and create objects of it with the diamond <>.
  • Write a generic method, and say how Java works out its type argument at each call.
  • Put a bound on a type parameter, as in <T extends Comparable<T>>, and say what the bound lets you call.
  • Explain why a List<Integer> is not a List<Number>, and choose between ? extends and ? super for a parameter.
  • Describe type erasure, what it rules out (new T(), instanceof List<String>, generic arrays), and why raw types are unsafe.
From before
You have used generic types since ArrayList: a list that can grow, where ArrayList<Integer> and the diamond were a recipe to copy. From wrapper classes you need Integer, Double and autoboxing. From polymorphism you need the downcast (String) x and the ClassCastException that a wrong downcast throws. The page also uses interfaces, static methods from static and final, and compareTo from strings.

A box that holds anything#

Picture a class whose only job is to hold one thing: a box. You put something in, and later you take it out. You want one Box class for every kind of thing, names one day and numbers the next.

Before generics, there was one way to write that. Every class extends Object, as you saw in inheritance, so a variable of type Object can refer to any object at all. A box that stores an Object can hold anything.

ObjectBox.java
1public class Main {
2 public static void main(String[] args) {
3 Box nameBox = new Box();
4 nameBox.put("Sana");
5 String name = (String) nameBox.get();
6 System.out.println("Hello, " + name);
7
8 Box ageBox = new Box();
9 ageBox.put(14);
10 int age = (Integer) ageBox.get();
11 System.out.println("Age next year: " + (age + 1));
12 }
13}
14
15class Box {
16 private Object item;
17
18 void put(Object item) {
19 this.item = item;
20 }
21
22 Object get() {
23 return item;
24 }
25}
Prints
›Hello, Sana
›Age next year: 15

It prints Hello, Sana and Age next year: 15. Step through it and watch the memory view: there are two Box objects, each with a field called item, one holding a String and one an Integer. Java boxed the 14 into an Integer for you, because an Object field can only hold objects.

Now look at the two lines that take something out. get() returns Object, so all Java knows is that some object came back. To use it as a String you downcast it with (String), and to use the age as a number you cast it with (Integer). Every read needs a cast.

When the wrong thing goes in

The casts are tedious, and they hide a worse problem: nothing stops a value of the wrong type from going into a box. Here a later part of the program puts a number into the box of names.

WrongItem.java
1public class Main {
2 public static void main(String[] args) {
3 Box nameBox = new Box();
4 nameBox.put("Sana");
5 greet(nameBox);
6
7 nameBox.put(42);
8 greet(nameBox);
9 }
10
11 static void greet(Box box) {
12 String name = (String) box.get();
13 System.out.println("Hello, " + name);
14 }
15}
16
17class Box {
18 private Object item;
19
20 void put(Object item) {
21 this.item = item;
22 }
23
24 Object get() {
25 return item;
26 }
27}
Prints
›Hello, Sana
!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 Main.greet(Main.java:12)
! at Main.main(Main.java:8)
Stops on line 12

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')

The object is not an instance of the class it was cast to.

It compiles. When you run it, the first greeting prints Hello, Sana, and the second call stops the program with a ClassCastException: an Integer cannot be cast to a String.

The mistake is on line 7, where 42 went into a box meant for names. The crash happens on line 12, inside a different method. In a real program those two lines could sit in different files, written by different people, and run hours apart. The compiler read both lines and said nothing, because put accepts any Object and the compiler takes your word for every cast.

A generic class: Box<T>#

Here is the same box written with generics. Two things change in the class: the name Box is followed by <T>, and every Object inside becomes T.

GenericBox.java
1public class Main {
2 public static void main(String[] args) {
3 Box<String> nameBox = new Box<>();
4 nameBox.put("Sana");
5 String name = nameBox.get();
6 System.out.println("Hello, " + name);
7
8 Box<Integer> ageBox = new Box<>();
9 ageBox.put(14);
10 int age = ageBox.get();
11 System.out.println("Age next year: " + (age + 1));
12 }
13}
14
15class Box<T> {
16 private T item;
17
18 void put(T item) {
19 this.item = item;
20 }
21
22 T get() {
23 return item;
24 }
25}
Prints
›Hello, Sana
›Age next year: 15

The output is the same, and there is no cast anywhere. nameBox.get() hands back a String, and ageBox.get() hands back an Integer, which Java unboxes into the int variable.

Reading the angle brackets

class Box<T> declares a type parameter called T. A type parameter is a placeholder for a type, the way a method parameter is a placeholder for a value. Inside the class, T sits wherever the type of the item goes: the field, the parameter of put and the return type of get. A class that declares type parameters is a generic class.

Box<String> fills the placeholder in. Here String is the type argument, and you read Box<String> aloud as “box of String”. For that box, every T in the class means String: put takes a String and get returns one. Box<Integer> is a box of Integer, made from the same class.

A method parameter
A type parameter
Declared in round brackets: void put(Object item)
Declared in angle brackets: class Box<T>
Stands for a value.
Stands for a type.
Filled in at each call: put("Sana")
Filled in where you use the class: Box<String>
The value you pass in is the argument.
The type you pass in is the type argument.

The mistake moves to compile time

Now try the bad line from before, putting 42 into the box of names:

Main.java
read along
1public class Main {
2 public static void main(String[] args) {
3 Box<String> nameBox = new Box<>();
4 nameBox.put("Sana");
5 nameBox.put(42);
6
7 String name = nameBox.get();
8 System.out.println("Hello, " + name);
9 }
10}
11
12class Box<T> {
13 private T item;
14
15 void put(T item) {
16 this.item = item;
17 }
18
19 T get() {
20 return item;
21 }
22}
Terminal
read along
1$ javac Main.java
2Main.java:5: error: incompatible types: int cannot be converted to String
3 nameBox.put(42);
4 ^
5Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
61 error

javac refuses to compile the program, and it points at line 5, where the wrong value goes in. It knows nameBox is a box of String, so put on that box takes a String, and 42 is an int. The Note line only says that javac shortened its message, and you can ignore it.

With the Object box, this mistake compiled, ran, and crashed somewhere else. Here it never gets past the compiler, and the error names the line where the bug really is.

Key idea

A type parameter is a placeholder for a type. You write the class once with T, each use fills in a real type, and the compiler checks every value that goes in. A wrong value becomes a compile error instead of a crash.

The Run button does not check generic types
The in-browser engine runs code without checking type arguments, so in the playground the program above would run and print Hello, 42. A real JDK refuses to compile it, as shown, and that is what to expect from Java.

A type argument must be an object type, never a primitive. You met this rule with ArrayList: Box<int> fails with unexpected type, so you write Box<Integer> and let autoboxing turn each int into an Integer. The section on type erasure shows where the rule comes from.

Quick check
Given Box<Integer> seats = new Box<>();, which line compiles?

Now add two methods to the box yourself. The second one uses T twice, as its parameter type and as its return type.

ExerciseMain.java
Finish isEmpty and getOrElse. A box is empty while its item is null. getOrElse hands back the item, or the fallback it was given when the box is empty. The parameter and the return type are both T, so a box of String takes a String fallback and a box of Integer takes an Integer.

Two type parameters, and the diamond#

A class can declare more than one type parameter, separated by commas inside the angle brackets. A pair that holds two values of different types needs two:

Pairs.java
1public class Main {
2 public static void main(String[] args) {
3 Pair<String, Integer> best = new Pair<>("Sana", 980);
4 Pair<String, String> contact = new Pair<>("Ravi", "555-0142");
5
6 String player = best.key();
7 int score = best.value();
8 System.out.println(player + " scored " + score);
9 System.out.println(contact.key() + ": " + contact.value());
10 }
11}
12
13class Pair<K, V> {
14 private final K key;
15 private final V value;
16
17 Pair(K key, V value) {
18 this.key = key;
19 this.value = value;
20 }
21
22 K key() {
23 return key;
24 }
25
26 V value() {
27 return value;
28 }
29}
Prints
›Sana scored 980
›Ravi: 555-0142

It prints Sana scored 980 and Ravi: 555-0142. In Pair<String, Integer> the first type argument fills in K and the second fills in V, by position, the same way method arguments fill parameters. So best.key() returns a String and best.value() returns an Integer. The constructor uses the type parameters too: Pair(K key, V value).

The same pair as a record
Records can be generic. record Pair<K, V>(K key, V value) {} gives you this whole class in one line, with the same key() and value() accessors, plus the equals, hashCode and toString you know from records.

Naming type parameters

By convention a type parameter is a single capital letter, so you can tell it from a real class at a glance: T is a placeholder, Task is a class. The Java library keeps to a handful of letters, and other Java programmers expect them:

LetterUsually stands forWhere you will see it
Ta type, any typeBox<T>, Comparable<T>
Ean element of a collectionList<E>, ArrayList<E>
K, Va key and its valueMap<K, V>
Na number<N extends Number>
S, Ua second and a third type<T, U>
javac accepts any name, but a word such as Item reads like the name of a real class.

The diamond

Before Java 7 you wrote the type arguments on both sides: Pair<String, Integer> best = new Pair<String, Integer>("Sana", 980);. Since Java 7 you can leave the brackets after new empty, and the compiler fills them in from the type of the variable. A compiler working out a type you did not write is called type inference, and the empty <> is the diamond you have typed since your first ArrayList.

No brackets at all is a different thing
new ArrayList() without the diamond still compiles, but it creates a raw type: a generic class used with its type checks switched off. javac then prints a note about “unchecked or unsafe operations”. Always write the diamond. The last section shows what can go wrong without it.

You have used generic types all along

The library's lists are declared just like your box. List is a generic interface whose declaration begins public interface List<E>, and ArrayList is a generic class whose declaration begins public class ArrayList<E> and which implements List<E>. When you wrote ArrayList<String>, you filled in E with String, so add took a String and get returned one, with no cast in sight.

Generic methods#

Sometimes a single method needs a type parameter while the class around it does not. Say you want the first item of any list: names, scores, anything. You could write firstName, firstScore and one more method for every type. A generic method declares its own type parameter and works for all of them.

FirstAndLast.java
1import java.util.List;
2
3public class Main {
4 public static void main(String[] args) {
5 List<String> names = List.of("Sana", "Ravi", "Mia");
6 List<Integer> scores = List.of(90, 75, 88);
7
8 String firstName = firstOf(names);
9 int firstScore = firstOf(scores);
10 System.out.println(firstName + " " + firstScore);
11
12 System.out.println(lastOf(names) + " " + lastOf(scores));
13 }
14
15 static <T> T firstOf(List<T> list) {
16 return list.get(0);
17 }
18
19 static <T> T lastOf(List<T> list) {
20 return list.get(list.size() - 1);
21 }
22}
Prints
›Sana 90
›Mia 88

It prints Sana 90 and Mia 88. Read the header static <T> T firstOf(List<T> list) from left to right. The <T> just before the return type declares a type parameter that belongs to this one method. Then T is the return type and List<T> is the parameter: give me a list of some type T, and I hand back one T.

The calls never say what T is. The compiler works it out from the argument. names is a List<String>, so in firstOf(names) T is String and the call returns a String. In firstOf(scores) T is Integer. This is type inference again, the same idea as the diamond.

Without the <T>, javac looks for a class called T
Delete the <T> and write static T firstOf(List<T> list), and javac reports cannot find symbol with symbol: class T. Nothing declared T as a type parameter, so javac reads it as the name of a class, and there is no class called T.
Put the lines in orderVotes.java
This generic method counts how many items in a list of any type equal a target. Put the lines in order so that the program prints 3.

Tap a line, then tap where it should go. Or use the arrows.

Inside count, the methods you can call on a T are the ones every object has, such as equals and toString, because T could be any type at all. The next section is about asking for more.

Bounded types: asking more of T#

Now write a method that finds the largest item in a list: the highest score, or the word that comes last in alphabetical order. You know compareTo from strings: a.compareTo(b) is negative when a comes first, zero when they are equal, and positive when a comes after. A first attempt:

Main.java
read along
1import java.util.List;
2
3public class Main {
4 public static void main(String[] args) {
5 System.out.println(max(List.of("kiwi", "apple", "plum")));
6 }
7
8 static <T> T max(List<T> items) {
9 T best = items.get(0);
10 for (T item : items) {
11 if (item.compareTo(best) > 0) {
12 best = item;
13 }
14 }
15 return best;
16 }
17}
Terminal
read along
1$ javac Main.java
2Main.java:11: error: cannot find symbol
3 if (item.compareTo(best) > 0) {
4 ^
5 symbol: method compareTo(T)
6 location: variable item of type T
7 where T is a type-variable:
8 T extends Object declared in method <T>max(List<T>)
91 error

javac cannot find compareTo, and the last line says why: T extends Object. With nothing else to go on, T could be any type, a Box as easily as a String, and the only methods every type is sure to have are those of Object. compareTo is not one of them.

Comparable: the types that can compare themselves

String, Integer and Double all have compareTo because each one implements a library interface called Comparable. It is a generic interface with a single method, and without its documentation comments it is three lines long:

Comparable.java
read along
1public interface Comparable<T> {
2 int compareTo(T other);
3}

String implements Comparable<String>, which says that a string can be compared with another string. Integer implements Comparable<Integer>.

Adding a bound

You can tell the compiler that T must be one of these comparable types. Write the requirement after the type parameter, with the word extends:

Max.java
1import java.util.List;
2
3public class Main {
4 public static void main(String[] args) {
5 List<String> fruits = List.of("kiwi", "apple", "plum");
6 List<Integer> scores = List.of(72, 95, 88);
7 List<Double> prices = List.of(2.5, 0.99, 4.25);
8
9 System.out.println(max(fruits));
10 System.out.println(max(scores));
11 System.out.println(max(prices));
12 }
13
14 static <T extends Comparable<T>> T max(List<T> items) {
15 T best = items.get(0);
16 for (T item : items) {
17 if (item.compareTo(best) > 0) {
18 best = item;
19 }
20 }
21 return best;
22 }
23}
Prints
›plum
›95
›4.25

It prints plum, 95 and 4.25. <T extends Comparable<T>> declares a bounded type parameter: T can be any type that can be compared with other values of type T. The part after extends is the bound. Every T is now sure to have compareTo, so the call inside the loop compiles.

In a bound, extends covers interfaces as well as classes. Comparable is an interface, and you still write extends, never implements.

Key idea

A bound limits which types can fill in a type parameter, and in return lets you call the bound's methods. <T extends Comparable<T>>means “any T that can be compared with another T”, so compareTo works on it.

The bound also checks every call

A bound works in two places. Inside the method it allows compareTo. At each call it checks that the type really qualifies. Here is a Song class with no compareTo, passed to the same max:

Main.java
read along
1import java.util.ArrayList;
2import java.util.List;
3
4public class Main {
5 public static void main(String[] args) {
6 List<Song> songs = new ArrayList<>();
7 songs.add(new Song("Echo"));
8 songs.add(new Song("Waves"));
9
10 Song top = max(songs);
11 System.out.println(top.title);
12 }
13
14 static <T extends Comparable<T>> T max(List<T> items) {
15 T best = items.get(0);
16 for (T item : items) {
17 if (item.compareTo(best) > 0) {
18 best = item;
19 }
20 }
21 return best;
22 }
23}
24
25class Song {
26 String title;
27
28 Song(String title) {
29 this.title = title;
30 }
31}
Terminal
read along
1$ javac Main.java
2Main.java:10: error: method max in class Main cannot be applied to given types;
3 Song top = max(songs);
4 ^
5 required: List<T>
6 found: List<Song>
7 reason: inference variable T has incompatible bounds
8 equality constraints: Song
9 upper bounds: Comparable<T>
10 where T is a type-variable:
11 T extends Comparable<T> declared in method <T>max(List<T>)
121 error

Read the reason as two demands that clash. equality constraints: Song says T has to be Song, because that is what the list holds. upper bounds: Comparable<T> says T has to be comparable. Song is not, so no T can meet both. To make songs work with max, Song has to implement Comparable<Song>, which you will do in Comparable, Comparator and sorting.

Predict the outputMax.java
1import java.util.List;
2
3public class Main {
4 public static void main(String[] args) {
5 System.out.println(max(List.of(3, 12, 7)));
6 System.out.println(max(List.of("3", "12", "7")));
7 }
8
9 static <T extends Comparable<T>> T max(List<T> items) {
10 T best = items.get(0);
11 for (T item : items) {
12 if (item.compareTo(best) > 0) {
13 best = item;
14 }
15 }
16 return best;
17 }
18}

The same max runs on a list of numbers and on a list of strings that look like the same numbers. Write the two lines it prints.

It prints 12, then 7. Each type brings its own compareTo. Integers compare by value, so 12 is the largest. Strings compare character by character from the left, so "7" beats both "3" and "12", whose first characters come before 7. max is the same code both times; only the compareTo that runs is different.

Now write a bounded method of your own.

ExerciseMain.java
Write isSorted, which returns true when every item in the list is less than or equal to the item after it. It must work for any comparable type, so it has the same bound as max. A list with one item counts as sorted.

Why a List<Integer> is not a List<Number>#

The number wrapper classes share a parent. Integer, Double, Long and the others all extend an abstract library class called Number. Its most useful method is doubleValue(), which gives any number back as a double. An Integer is-a Number, so Number n = 14; compiles: Java boxes 14 into an Integer and stores it in the Number variable.

If every Integer is a Number, is a list of Integers a list of Numbers? It sounds obvious. javac disagrees:

Main.java
read along
1import java.util.ArrayList;
2import java.util.List;
3
4public class Main {
5 public static void main(String[] args) {
6 List<Integer> scores = new ArrayList<>(List.of(90, 75));
7 List<Number> numbers = scores;
8 numbers.add(2.5);
9 int last = scores.get(2);
10 }
11}
Terminal
read along
1$ javac Main.java
2Main.java:7: error: incompatible types: List<Integer> cannot be converted to List<Number>
3 List<Number> numbers = scores;
4 ^
51 error

Pretend javac allowed line 7. Then numbers and scores would be two names for one list, like the shared references in object references. Line 8 adds 2.5 through numbers, which is fine for a list of Numbers. But it is the same list as scores, which promised to hold only Integers, and line 9 would take the 2.5 out as an int. The list would break its promise without a single cast in the code, so javac stops the program at the first step, line 7.

Think of a bag of apples. Taking fruit out of it is always fine. But if you hand it to someone as “a bag of fruit”, they may drop a banana in, and the next person who reaches in for an apple gets a banana.

The rule has a name. Generic types are invariant: List<Integer> and List<Number> are two separate types with no is-a link between them, even though Integer is-a Number. The class part can still change: an ArrayList<Integer> is a List<Integer>, because the type argument stays the same.

Quick check
Which of these lines compiles?

Wildcards: ? extends and ? super#

Invariance keeps lists honest, and sometimes it gets in the way. A method static double sum(List<Number> numbers) that adds up a list cannot accept a List<Integer>, for the reason you just saw. Yet sum only reads the numbers, and reading an Integer as a Number is always safe. It was adding that caused the trouble.

? extends: a list you read from

Write the parameter as List<? extends Number>. The question mark is a wildcard: it stands for one particular type that the method does not name. ? extends Number means “some type that is Number or a subclass of it”, so a List<Integer>, a List<Double> and a List<Number> all fit. Whatever the real type is, every item in the list is at least a Number, so the method can read each one as a Number.

Sum.java
1import java.util.List;
2
3public class Main {
4 public static void main(String[] args) {
5 List<Integer> scores = List.of(90, 75, 88);
6 List<Double> prices = List.of(2.5, 4.0);
7
8 System.out.println(sum(scores));
9 System.out.println(sum(prices));
10 }
11
12 static double sum(List<? extends Number> numbers) {
13 double total = 0;
14 for (Number n : numbers) {
15 total = total + n.doubleValue();
16 }
17 return total;
18 }
19}
Prints
›253.0
›6.5

It prints 253.0 and 6.5: one method, two kinds of list. The cost is that you cannot add to such a list. Inside sum, the list might really be a List<Double>, and then adding an Integer would be the same broken promise as before:

Main.java
read along
1import java.util.ArrayList;
2import java.util.List;
3
4public class Main {
5 public static void main(String[] args) {
6 List<Integer> scores = new ArrayList<>(List.of(90, 75));
7 addBonus(scores);
8 }
9
10 static void addBonus(List<? extends Number> numbers) {
11 numbers.add(5);
12 }
13}
Terminal
read along
1$ javac Main.java
2Main.java:11: error: incompatible types: int cannot be converted to CAP#1
3 numbers.add(5);
4 ^
5 where CAP#1 is a fresh type-variable:
6 CAP#1 extends Number from capture of ? extends Number
7Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
81 error

CAP#1is javac's name for the one real type behind the question mark, which this method cannot see (“capture of ? extends Number”). It might be Integer, Double or Number, and 5 only fits some of those, so javac refuses. A ? extends list is for reading. The one value it accepts is null, which fits every type.

? super: a list you write into

The opposite job comes up too. The method below adds ticket numbers 1, 2, 3 and so on to a list. It works for a list of Integers, and it would be just as correct to add the tickets to a list of Numbers or a list of plain Objects, since an Integer is both.

Write that parameter as List<? super Integer>: “a list of some type that is Integer or a superclass of it”. That allows List<Integer>, List<Number> and List<Object>. Each of those can hold an Integer, so adding one is always safe.

Tickets.java
1import java.util.ArrayList;
2import java.util.List;
3
4public class Main {
5 public static void main(String[] args) {
6 List<Integer> tickets = new ArrayList<>();
7 List<Number> measurements = new ArrayList<>(List.of(2.5));
8 List<Object> log = new ArrayList<>(List.of("start"));
9
10 addTickets(tickets, 3);
11 addTickets(measurements, 2);
12 addTickets(log, 2);
13
14 System.out.println(tickets);
15 System.out.println(measurements);
16 System.out.println(log);
17 }
18
19 static void addTickets(List<? super Integer> target, int count) {
20 for (int i = 1; i <= count; i++) {
21 target.add(i);
22 }
23 }
24}
Prints
›[1, 2, 3]
›[2.5, 1, 2]
›[start, 1, 2]

It prints [1, 2, 3], [2.5, 1, 2] and [start, 1, 2]. Step through it and watch the three lists grow in the memory view, each one through the same method.

This time reading is the weak side. An item taken out of a List<? super Integer> could be any object, the String "start" included, so get gives you back only an Object.

Both at once: producer extends, consumer super

Many methods use both kinds. Take copying, where every item of one list goes into another. The source list hands items to the method, and the target list takes items from it. Give each parameter the wildcard for its job:

CopyInto.java
1import java.util.ArrayList;
2import java.util.List;
3
4public class Main {
5 public static void main(String[] args) {
6 List<Integer> scores = List.of(90, 75);
7 List<Double> prices = List.of(2.5, 4.0);
8
9 List<Number> measurements = new ArrayList<>();
10 copyInto(scores, measurements);
11 copyInto(prices, measurements);
12 System.out.println(measurements);
13
14 List<Object> everything = new ArrayList<>(List.of("start"));
15 copyInto(measurements, everything);
16 System.out.println(everything);
17 }
18
19 static <T> void copyInto(List<? extends T> from, List<? super T> to) {
20 for (T item : from) {
21 to.add(item);
22 }
23 }
24}
Prints
›[90, 75, 2.5, 4.0]
›[start, 90, 75, 2.5, 4.0]

It prints [90, 75, 2.5, 4.0] and [start, 90, 75, 2.5, 4.0]. The parameter from is a producer: the method only takes items out of it, so its type is ? extends T, which accepts a list of T or of any subtype of T. The parameter to is a consumer: the method only puts items into it, so its type is ? super T, which accepts a list of T or of any supertype. That is how one method copies Integers into a list of Numbers, and Numbers into a list of Objects.

Make both parameters a plain List<T> and none of the three calls compiles. For the first one javac reports inference variable T has incompatible equality constraints Number,Integer: T would have to be Integer and Number at the same time.

Java programmers remember the rule with a phrase from Joshua Bloch's book Effective Java: producer extends, consumer super, often shortened to PECS. Before you choose a wildcard, ask what the method does with the parameter: does it take items out, put items in, or both?

The methodParameter typeLists that fitWhat it can do
only reads numbersList<? extends Number>List<Number>, List<Integer>, List<Double>Read items as Number. Add nothing but null.
only adds IntegersList<? super Integer>List<Integer>, List<Number>, List<Object>Add Integers. Read items only as Object.
reads and adds numbersList<Number>List<Number>Read items as Number, add any Number.
only prints, counts or clearsList<?>a list of any typeRead items as Object. Add nothing but null.
List<?> on its own is the unbounded wildcard: a list of some type that the method does not care about.
Key idea

If a method only reads from a parameter, give it ? extends. If it only writes into it, give it ? super. If it does both, use the exact type.

Wildcards belong on parameters
Use wildcards for method parameters, and not for return types. A method that returns List<? extends Number> forces every caller to deal with an unknown type, so return a plain List<Number> or List<T> instead.
Quick check
A method fillWithZeros adds 0 to a list several times and never reads from it. Which parameter type accepts a List<Integer>, a List<Number> and a List<Object>?

Type erasure: what is left when the program runs#

The type arguments you write are there for the compiler. javac uses them to check every put and get, and then, as it writes the instructions of the compiled class, it removes them. This step is called type erasure, and it does three things:

  • It replaces each type parameter with its bound, or with Object when there is no bound. In Box<T> every T becomes Object. With <T extends Comparable<T>> every T becomes Comparable.
  • It drops the type arguments from the code that uses the class. Box<String> and Box<Integer> both become plain Box.
  • Wherever your code reads a T and uses it as a particular type, it inserts the cast you used to write by hand.

So the compiled program works much like the Object box from the start of this page, with one difference: every cast in it was added by the compiler, after it checked that the cast cannot fail. Switch between the two views below, and step through the changes one at a time.

Erasure viewBox.java
Example
3
5 this.item = item;
6 }
7
9 return item;
10 }
11}
12
13// somewhere in main
15drink.put("tea");
17
replaced or removed by erasureadded by the compiler
Change 1 of 5

The type parameter list is gone. The JVM gets one ordinary class called Box, whatever type arguments the rest of the program uses.

Seeing it at run time

Predict the outputSameClass.java
1import java.util.ArrayList;
2import java.util.List;
3
4public class Main {
5 public static void main(String[] args) {
6 List<String> words = new ArrayList<>();
7 List<Integer> numbers = new ArrayList<>();
8
9 System.out.println(words.getClass().getSimpleName());
10 System.out.println(numbers.getClass().getSimpleName());
11 System.out.println(words.getClass() == numbers.getClass());
12 }
13}

getClass() asks an object for its class while the program runs, and getSimpleName() gives that class's short name. Which output is right?

Both names come out as ArrayList, and the comparison prints true: the two lists are objects of one and the same class. Nothing inside an ArrayList object records whether it was made as a list of String or a list of Integer. The compiler used that information and then erased it.

What erasure rules out

Because the running program does not know what T is, a few things that look reasonable cannot compile. Here are three of them in one class:

Shelf.java
read along
1import java.util.List;
2
3class Shelf<T> {
4 T makeOne() {
5 return new T();
6 }
7
8 boolean holdsWords(Object thing) {
9 return thing instanceof List<String>;
10 }
11
12 T[] makeSlots() {
13 return new T[10];
14 }
15}
Terminal
read along
1$ javac Shelf.java
2Shelf.java:5: error: unexpected type
3 return new T();
4 ^
5 required: class
6 found: type parameter T
7 where T is a type-variable:
8 T extends Object declared in class Shelf
9Shelf.java:9: error: Object cannot be safely cast to List<String>
10 return thing instanceof List<String>;
11 ^
12Shelf.java:13: error: generic array creation
13 return new T[10];
14 ^
153 errors
  • new T(): to build an object, Java needs its class while the program runs, and T has been erased. There is no class whose constructor it could call.
  • thing instanceof List<String>: a list object does not remember its type argument, so this test is impossible. javac words it as an unsafe cast, because instanceof checks whether a cast would work. thing instanceof List<?> compiles, since it only asks whether the object is a list of some kind.
  • new T[10], and also new List<String>[3]: an array checks every value stored in it while the program runs, as the ArrayStoreException earlier showed, and it cannot check a type that has been erased. Use an ArrayList<T> instead.

Erasure explains a few more rules you may run into:

You writejavac saysWhy
Box<int>unexpected typeAfter erasure a T is stored as an Object, and an int is not an object. Write Box<Integer>.
static T latest; inside Box<T>non-static type variable T cannot be referenced from a static contextOne static field is shared by every Box, whether it holds Strings or Integers, so its type cannot be T.
print(List<String>) and print(List<Integer>)name clash: ... have the same erasureAfter erasure both methods are print(List), and a class cannot have two methods with the same name and parameters.
Key idea

The compiler checks type arguments and then erases them, so the running program never knows what T is. Anything that would need T while the program runs, such as new T(), instanceof List<String> or new T[10], cannot compile.

Quick check
In the compiled code, what type does the field value have in class Holder<T extends Number> { T value; }?

Raw types and the unchecked warning

A raw type is a generic type written with no angle brackets at all: List instead of List<String>. Raw types exist so that code written before Java 5 still compiles. Using one switches the checks off, and javac warns you about it. This program uses a raw type to slip a number into a list of names:

Main.java
read along
1import java.util.ArrayList;
2import java.util.List;
3
4public class Main {
5 public static void main(String[] args) {
6 List<String> names = new ArrayList<>();
7 names.add("Sana");
8
9 List raw = names;
10 raw.add(42);
11 System.out.println(names);
12
13 String first = names.get(0);
14 String second = names.get(1);
15 System.out.println(first + " and " + second);
16 }
17}
Terminal
read along
1$ javac Main.java
2Note: Main.java uses unchecked or unsafe operations.
3Note: Recompile with -Xlint:unchecked for details.
4$ java Main
5[Sana, 42]
6Exception 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')
7 at Main.main(Main.java:14)

javac compiles it, with only a two-line note. At run time the list holds plain objects, so line 10 stores the 42 without complaint, and printing shows [Sana, 42]. The crash comes on line 14, a line with no cast in it. The cast is the one erasure inserted: in the compiled code names.get(1) returns Object, and javac added (String) in front of it. The Object box problem from the start of this page is back, because one raw type switched the checks off.

The in-browser engine does not insert the casts that javac adds, so this program is read-along. The note tells you how to see the details: compile again with -Xlint:unchecked.

Terminal
read along
1$ javac -Xlint:unchecked Main.java
2Main.java:10: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
3 raw.add(42);
4 ^
5 where E is a type-variable:
6 E extends Object declared in interface List
71 warning

“Unchecked” means javac could not check the call, so the safety that generics promise no longer holds for it.

Treat unchecked warnings as bugs
Never write a raw type in new code, and always write the diamond: new ArrayList<>(), not new ArrayList(). When javac prints the “unchecked or unsafe operations” note, compile again with -Xlint:unchecked and fix every warning it lists. When your whole program compiles with no unchecked warnings, and you have not hidden any, the casts that erasure inserts can never fail.
Next: every collection is generic
The next lesson, the Collections Framework, is built from what you learned here: List<E>, Set<E>, Queue<E> and Map<K, V> are all generic interfaces, and their methods use the wildcards from this page.

Try it

Answer each one in your head or on paper first, then open the answer.

1
In Pair<String, Integer> best = new Pair<>("Sana", 980);, name the type parameters, the type arguments, and what the diamond stands for.
2
What does the <T> just before the return type in static <T> T firstOf(List<T> list) do, and what happens if you delete it?
3
What does this program print? Then explain why printAll takes a List<?> and not a List<Object>.
4
A method totalWeight reads a list of parcel weights and adds them up. A method addSampleCodes puts Integer codes into a list and never reads it. Which wildcard does each parameter get?
5
Why does if (thing instanceof List<String>) not compile, and what can you write instead?

Practice

Work through these on your own. Each one builds on this page.

1
Write a generic class Shelf<T> that holds at most a fixed number of items. Give it a constructor that takes the capacity, boolean add(T item) that returns false when the shelf is full, T get(int index) and int size(). Keep the items in an ArrayList<T>. Try it with a Shelf<String> of capacity 2 and a Shelf<Integer>.
2
Write static <T extends Comparable<T>> T min(List<T> items) and static <T extends Comparable<T>> int countAbove(List<T> items, T limit). Test both with a list of Integer and a list of String. Then call min with a list of the Song class from this page in a real JDK, and explain javac's message in your own words.
3
Write static double average(List<? extends Number> numbers). Store some scores in a variable of type List<Integer> and some prices in a variable of type List<Double>, and pass each variable to average. Then change the parameter to List<Number>, compile with a real JDK, and copy the error. In one sentence, explain why the wildcard version accepts both lists.
4
Add a generic method static <K, V> Pair<V, K> swap(Pair<K, V> pair) to the Pair program. It returns a new pair with the key and the value exchanged. What type does swap(best) have when best is a Pair<String, Integer>? Print its key and its value to check.

Further reading#

Spotted a mistake, or a part that did not make sense? Report it by email