Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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

This 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:

3

StringBuilder::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 elements

Writing 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 failed

Give the methods different names, for example printWords() and printNumbers().

Thanks for reading!

Donate to the site — we work hard for you.