How it works
The explanation, with examples you can run.
What the bytecode sees
List<String> names = new ArrayList<>();names.add("Ada");String first = names.get(0);compiles to roughly what you would have written before Java 5:
List names = new ArrayList();names.add("Ada");String first = (String) names.get(0); // checkcast inserted by javacSo List<String> and List<Integer> share one runtime class, ArrayList. Type arguments do survive in the class file as metadata (the Signature attribute), so reflection can tell you a field was declared List<String>. But a List object at run time doesn't know what it was created for.
When erasure leaks
The compiler warns about raw types and unchecked casts because they let the wrong type in without stopping it:
import java.util.*; public class Erasure { public static void main(String[] args) { List<String> names = new ArrayList<>(); List<Integer> counts = new ArrayList<>(); System.out.println(names.getClass() == counts.getClass()); // true List raw = names; // raw type: a warning, not an error raw.add(42); // heap pollution: an Integer in a List<String> try { String first = names.get(0); // the hidden cast fails here } catch (ClassCastException e) { System.out.println("ClassCastException at the read, not at the add"); } }}The failure appears where the value is read, which may be in another module, long after the bad add. Treat unchecked warnings as bugs until proven otherwise; @SuppressWarnings("unchecked") should come with a reason.
Erasure also explains these rules:
- No
new T()orT.class: there is noTat run time. Pass aClass<T>or aSupplier<T>instead. - No
List<int>: type variables erase toObject, and anintisn't one. You getList<Integer>and pay for boxing. - No generic arrays:
new T[10]is illegal, because arrays check their element type at run time and would need to knowT. - No overloading on type arguments:
void f(List<String>)andvoid f(List<Integer>)erase to the same signature and won't compile together.
When a generic class is subclassed with a concrete type, for example class Name implements Comparable<Name>, javac also generates a bridge method compareTo(Object) that casts and calls your compareTo(Name), so callers that only know the erased interface still reach your code. You can see it with javap -p.
Wildcards
Generic types are invariant: a List<Integer> is not a List<Number>, or you could add a Double to it. Wildcards give the flexibility back:
static double sum(Collection<? extends Number> xs) { // read Numbers out double total = 0; for (Number x : xs) total += x.doubleValue(); return total;} static void fill(List<? super Integer> xs) { // put Integers in for (int i = 0; i < 3; i++) xs.add(i);}The rule of thumb: extends when the collection produces values for you, super when it consumes them. Collections.copy(List<? super T> dest, List<? extends T> src) uses both.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.