PECS and type erasure
The bounded types lesson introduced the ? extends and ? super wildcards. Here we look at the rule for choosing between them, and at type erasure — a feature of Java that makes generic types behave differently at run time than you might expect.
The PECS rule
The acronym PECS helps you remember which wildcard to use: Producer Extends, Consumer Super. If a collection produces values for the method (the method reads from it), write ? extends T. If it consumes values (the method writes to it), write ? super T:
<T> void copy(List<? extends T> source, List<? super T> target) {
for (T item : source) {
target.add(item);
}
}
void main() {
List<Integer> ints = List.of(1, 2, 3);
List<Number> numbers = new ArrayList<>();
copy(ints, numbers);
IO.println(numbers);
}Output:
[1, 2, 3]Thanks to the wildcards, the method copies integers into a list of Number or even Object. The standard Collections.copy() is built this way.
The unbounded ?
List<?> means "a list of anything". It fits when the method does not care about the element type: elements are read as Object, and nothing can be added to such a list:
void printAll(List<?> items) {
for (Object item : items) {
IO.print(item + " ");
}
IO.println("(" + items.size() + ")");
}
void main() {
printAll(List.of("a", "b"));
printAll(List.of(1, 2, 3));
}Output:
a b (2)
1 2 3 (3)Type erasure
Type parameters exist only for the compiler. After checking the types it erases them: in the running program List<String> and List<Integer> are the same ArrayList class:
void main() {
List<String> words = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
IO.println(words.getClass() == numbers.getClass());
IO.println(words.getClass().getName());
}Output:
true
java.util.ArrayListThis kept generic types compatible with old code written before Java 5. But it has consequences.
No new T()
At run time nobody knows what T is, so new T() does not compile. The solution is to pass the method an object "factory", a Supplier<T>:
<T> List<T> fill(Supplier<T> factory, int count) {
List<T> result = new ArrayList<>();
for (int i = 0; i < count; i++) {
result.add(factory.get());
}
return result;
}
void main() {
List<StringBuilder> buffers = fill(StringBuilder::new, 3);
IO.println(buffers.size());
}Output:
3StringBuilder::new is a constructor reference; it creates a new object on every get() call. For the same reason you cannot create an array with new T[10].
instanceof and generic types
You cannot check that an object is exactly a List<String>: the information about String has been erased. The compiler only allows a check without the parameter — List<?>:
void main() {
Object value = List.of("a", "b");
if (value instanceof List<?> list) {
IO.println("A list of " + list.size() + " elements");
}
}Output:
A list of 2 elementsWriting value instanceof List<String> for a variable of type Object causes the compile error Object cannot be safely cast to List<String>.
Common mistake
Because of erasure, two methods that differ only in a type parameter look identical after compilation. Java will not allow such an overload:
void print(List<String> words) {
IO.println("strings");
}
void print(List<Integer> numbers) {
IO.println("numbers");
}
void main() {
}Output:
Main.java:5: error: name clash: print(List<Integer>) and print(List<String>) have the same erasure
void print(List<Integer> numbers) {
^
1 error
error: compilation failedGive the methods different names, for example printWords() and printNumbers().