Java generics
How type erasure works in Java generics
Generics are a compile-time device. The compiler checks your types, inserts casts where they are needed, then erases each type parameter to its bound, or to Object when it is unbounded. So List<String> and List<Integer> are the same class at run time.
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // true: one runtime class
Because the type argument is gone at run time, several things you might reach for do not exist: you cannot write new T[n], call T.class, or test x instanceof List<String>. The type variable has no class to stand on. The usual workaround passes a Class<T> token so the run time has a real type to use.
Why List<String> is not a List<Object>
Generic types are invariant. Although String is a subtype of Object, List<String> is not a subtype of List<Object>, and the two are unrelated types.
List<String> names = new ArrayList<>();
List<Object> objs = names; // does not compile
Wrong "A List<String> is a List<Object>, so I can pass one where the other is wanted." Assigning one to the other does not compile, and their common supertype is List<?>, not List<Object>. Right treat each parameterization as its own type, and accept a family of them with a wildcard.
Wildcards: extends to read, super to write
A wildcard ? means "some unknown type." An upper-bounded List<? extends Number> is a producer you read from: every element is a Number or below, so you can read one as Number, but you cannot add, because the real element type is an unknown subtype.
double sum(List<? extends Number> src) {
double t = 0;
for (Number n : src) t += n.doubleValue(); // read as Number: OK
// src.add(1); // does not compile: read-only
return t;
}
A lower-bounded List<? super Integer> is a consumer you write to: it holds Integer or a supertype, so you can add an Integer, but a read comes back only as Object.
void addInts(List<? super Integer> dst) {
dst.add(1); // write an Integer: OK
Object o = dst.get(0); // read comes back as Object
// Integer i = dst.get(0); // does not compile
}
PECS and generic methods
The rule for picking a wildcard is PECS: Producer Extends, Consumer Super. A parameter that is a source you read from takes ? extends; a sink you write into takes ? super; a parameter you do both to takes a plain type parameter. Collections.copy shows both at once:
static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T item : src) dest.add(item); // read the producer, write the consumer
}
A generic method declares its own type parameter before the return type, and T is inferred from the arguments, so callers write copy(dst, src) with no explicit type. Construct the lists with the diamond, new ArrayList<>(), since a raw new ArrayList() drops back to unchecked generics.
Erasure restrictions and the Class<T> type token
Everything erasure forbids follows from the type argument being gone at run time.
T[] a = new T[n]; // does not compile
Class<T> c = T.class; // does not compile
if (x instanceof List<String>) {} // does not compile: only List<?> or raw List
The standard workarounds hand the run time a real type: pass a Class<T> token and call cls.getDeclaredConstructor().newInstance(), or allocate (T[]) new Object[n] with an unchecked cast, or Array.newInstance(cls, n). A type variable also cannot be a static field, and a generic class cannot extend Throwable. Separately, catch (T e) is illegal because erasure leaves the JVM unable to tell exception types apart at run time, even though throws T is allowed. Non-reifiable types like List<String> are the reason unchecked-cast warnings and heap pollution exist.
Bridge methods and the raw-type ClassCastException
When a class extends a parameterized type, erasure can break the override, so the compiler synthesizes a bridge method.
class Node<T> { void set(T t) {} } // erases to set(Object)
class IntNode extends Node<Integer> {
void set(Integer i) {} // would not override set(Object) after erasure
}
// compiler-generated bridge in IntNode:
// void set(Object o) { set((Integer) o); }
The bridge restores polymorphism, and it hides a landmine. Reach IntNode through a raw Node and the inserted cast fires:
Node n = new IntNode(); // raw type: unchecked warning
n.set("oops"); // ClassCastException at run time
The cast that throws is the one the compiler inserted during erasure, not one you wrote.
Invariance is the fix for unsound array covariance
Arrays are covariant and generics are invariant, by design.
Number[] arr = new Integer[2]; // compiles: arrays are covariant
arr[0] = 3.14; // ArrayStoreException at run time
List<Number> ln = new ArrayList<Integer>(); // does not compile
An array keeps its element type at run time and checks every store, so the unsound assignment is caught late by ArrayStoreException. Generics reject the analogous line at compile time, which is the reason they were made invariant. Wildcards then recover variance at the use site: List<Integer> is a List<? extends Number>, and List<Object> is a List<? super Integer>.
Wildcard limits: one bound, and super only on wildcards
The read/write rules have hard edges.
List<? extends Number> p = someInts;
p.add(null); // the only value add accepts
List<? super Integer> c = someNumbers;
Object o = c.get(0); // a read is typed only as Object
Wrong "? super lets me read the bounded type back." A read from List<? super Integer> is typed Object, because the list could be List<Object>; only writes see the Integer bound. A wildcard carries one bound, never both, so there is no <? extends A super B>. And super belongs to wildcards alone: a named type parameter can be written <T extends X> but never <T super X>.
Recursive and multiple bounds, and erasure of the leftmost bound
A bound can refer to the type variable itself, and it can list several types.
<T extends Comparable<T>> // recursive bound: T compares to itself
<T extends Number & Comparable<T>> // multiple bounds; a class must come first
With multiple bounds the type variable is a subtype of all of them, at most one may be a class, and a class bound has to precede the interfaces. Erasure keeps the leftmost bound, so T here erases to Number. Collections.max is declared <T extends Object & Comparable<? super T>>: the leading Object forces T to erase to Object for binary compatibility, while Comparable<? super T> lets T be comparable through a supertype.
beta
The interviewer part is in the works.
The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.