Generics, Type Erasure and Wildcards
What the compiler removes and what it inserts, why you cannot create an array of a generic type, PECS explained by what it enables, bridge methods, and heap pollution from unchecked varargs.
On this page
Generics are compile-time only. Understanding exactly what the compiler removes, and exactly what it inserts in its place, explains every restriction the language imposes — and every restriction is a potential interview question.
Key Takeaways
- Erasure replaces
Twith its leftmost bound (Objectif unbounded) and inserts casts at every read site. - At runtime
List<String>andList<Integer>are the same class. There is no type argument to inspect. - PECS:
? extends Tto read,? super Tto write. You cannot do both through a wildcard. - You cannot instantiate
new T(),new T[n], catch a generic exception, or overload on erasure- identical signatures. - Heap pollution is a
List<String>that actually contains a non-String. Generic varargs are the usual route in.
What the compiler actually does
public class Box<T extends Comparable<T>> {
private T value;
public T get() { return value; }
public void set(T value) { this.value = value; }
}
Box<String> box = new Box<>();
box.set("hello");
String s = box.get();public class Box {
private Comparable value; // T -> its leftmost bound
public Comparable get() { return value; }
public void set(Comparable value) { this.value = value; }
}
Box box = new Box();
box.set("hello");
String s = (String) box.get(); // cast inserted by the compilerTwo moves: the type parameter is replaced by its bound, and a checked cast is inserted wherever a generic value is read. Generics are therefore not a runtime feature at all — they are a static guarantee plus automatic casts, which is exactly the code you would have written by hand in Java 1.4.
Everything that follows is a consequence.
The restrictions, and why each exists
| You cannot | Because |
|---|---|
new T() | No class object for T at runtime |
new T[10] | The array would have no reifiable component type |
o instanceof List<String> | The type argument is gone; only the raw type survives |
catch (MyException<T> e) | The JVM matches exceptions by exact runtime class |
void f(List<String>) and void f(List<Integer>) | Both erase to f(List) — a duplicate method |
A static field of type T | Statics are per-class, and there is one class for all arguments |
The array restriction deserves a moment, because it is the one that comes up in real code. Java arrays are covariant and reified — they know their component type and check it on every store:
Object[] objects = new String[1]; // legal: arrays are covariant
objects[0] = 42; // compiles, throws ArrayStoreException at runtime
// If this were legal:
List<String>[] lists = new List<String>[1]; // it is not
Object[] objs = lists;
objs[0] = List.of(1, 2, 3); // no ArrayStoreException — type argument is erased
String s = lists[0].get(0); // ClassCastException, far from the real mistakeThe array's runtime check cannot see the type argument, so the error surfaces at an unrelated read
instead of the bad write. Java forbids the creation outright rather than allow that. In practice you
write List<List<String>>, or create (T[]) new Object[n] inside a class that keeps the array
private — which is precisely what ArrayList does.
PECS
The rule is Producer Extends, Consumer Super, and it is best learned from what it makes possible.
// Without wildcards, this only accepts List<Number> — not List<Integer>.
public static double sum(List<? extends Number> source) { // PRODUCER: we read
double total = 0;
for (Number n : source) total += n.doubleValue();
return total;
// source.add(1) would not compile: the compiler knows the list is
// *some* subtype of Number but not which, so no value is safe to add.
}
// Accepts List<Integer>, List<Number> or List<Object> — anything Integers fit into.
public static void fill(List<? super Integer> target, int count) { // CONSUMER: we write
for (int i = 0; i < count; i++) target.add(i);
// Integer x = target.get(0) would not compile: reads come back as Object.
}The asymmetry is the point. Through ? extends Number you can read a Number but write nothing;
through ? super Integer you can write an Integer but read only Object. A parameter that must do
both takes a plain T.
Collections.copy(List<? super T> dest, List<? extends T> src) is the canonical illustration — one
parameter of each kind in one signature. The JDK is full of these; noticing the pattern in a
signature you already use makes the rule stick.
Bridge methods
Erasure breaks polymorphism in one place, and the compiler patches it invisibly.
class Box<T> {
void set(T value) { }
}
class StringBox extends Box<String> {
@Override void set(String value) { } // erases to set(String)
}Box.set erases to set(Object). StringBox.set is set(String). Different signatures, so the JVM
would not treat one as overriding the other, and a call through a Box reference would dispatch to
the wrong method. The compiler therefore generates a synthetic bridge method in StringBox:
void set(Object value) { // bridge: matches the erased superclass signature
set((String) value); // casts and delegates — this is where a
} // ClassCastException from a raw-type call originatesYou see bridge methods in stack traces, in reflection output (Method.isBridge()), and as the source
of a ClassCastException that appears to come from a line with no cast on it. That last case only
happens when someone passes a Box raw reference holding a StringBox and calls set(42).
Heap pollution and unchecked varargs
Heap pollution is when a variable of parameterised type refers to an object that is not of that type. The compiler warns, then erasure lets it happen anyway.
@SafeVarargs // asserting this is safe — the author's responsibility
static <T> List<T> listOf(T... items) { return Arrays.asList(items); }
static <T> T[] toArray(T... args) { return args; } // args is really Object[]
static <T> T[] pick(T a, T b) {
return toArray(a, b); // creates an Object[], not a T[]
}
String[] strings = pick("x", "y"); // ClassCastException at this lineA generic varargs parameter is compiled to an array of the erased type — Object[] here — so
returning it as T[] is a lie the compiler cannot check. @SafeVarargs suppresses the warning and is
an assertion by the author that the method never stores anything into the array and never lets it
escape. Applying it to a method that does either is how the hole gets opened.
Raw types, and why they are still around
List without a type argument is a raw type: legal for compatibility with pre-Java-5 code,
warned about by every compiler, and toxic in one specific way — using a raw type disables generic
checking for the entire expression, not just the missing argument.
List raw = new ArrayList<String>();
raw.add(42); // unchecked warning only
List<String> safe = raw; // unchecked warning only
String s = safe.get(0); // ClassCastException — far from the actual mistakeIf you genuinely need a list of unknown element type, use List<?>. It is type-safe: you can read
elements as Object and the only thing you may add is null. That distinction — raw List versus
List<?> — is a reliable interview question, and the answer is that one turns checking off and the
other turns it up.
Frequently Asked Questions
Why does Java use erasure instead of reified generics?
What does PECS actually stand for and when do I use it?
Can I do instanceof List of String at runtime?
Related tutorials
- static, final and Class Initialisation OrderThe exact order the JVM initialises a class, why a static final String can survive deleting the class that declared it, effectively final, and how two classes can deadlock while loading.
- Immutable Objects & Defensive CopyingThe five conditions for a genuinely immutable class, the leaked-collection bug that defeats final, why records are only shallowly immutable, and what final fields guarantee across threads.
- Exception Handling, finally and try-with-resourcesChecked versus unchecked and when each is right, how finally silently discards an exception or a return value, and suppressed exceptions in try-with-resources.
- Comparable, Comparator and Sorting ContractsNatural order versus external order, the total-ordering contract and the exception thrown when you break it, comparator chaining, the integer-overflow bug, and TimSort stability.