Memra

Type erasure & its consequences

◈ 5 cards

Generics are a compile-time fiction — erasure removes them at runtime, which forbids new T(), generic arrays, and runtime type tests.

What erasure is

Java generics are implemented by type erasure: the type parameters exist only at compile time. After the compiler verifies your types and inserts casts, it erases the parameters from the bytecode. List<String> and List<Integer> become the same class — plain List — at runtime:

List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // true — both are ArrayList

An unbounded T erases to Object; a bounded <T extends Number> erases to its leftmost bound, Number. This design kept generics backward-compatible with pre-1.5 bytecode, but it means the JVM has no idea about your type arguments. Several things follow directly.

What you cannot do, and why

1. new T() is illegal. The runtime does not know what T is, so it cannot allocate one:

<T> T make() { return new T(); }   // COMPILE ERROR

2. instanceof T and instanceof List<String> are illegal. The runtime type carries no parameter, so the test is meaningless. Only the unbounded form is allowed:

if (x instanceof List<String>) {} // COMPILE ERROR
if (x instanceof List<?>) {}       // OK — no parameter to check

3. You cannot create a generic array. new T[10] and new List<String>[10] are forbidden, because arrays know and check their element type at runtime but generics do not — the two models clash. Use a collection (List<T>) or an Object[] with casts instead.

The Class<T> token workaround

Because new T() is impossible, the idiom is to pass the class object as a runtime token. The Greenhouse project uses exactly this to build an Event from a class — and a generic factory looks like:

static <T> T create(Class<T> type) throws Exception {
    return type.getDeclaredConstructor().newInstance();
}
String s = create(String.class);   // type token supplies the runtime type

Class<T> survives erasure (it is an ordinary object you pass in), so newInstance() has the runtime type information that new T() lacks.

One more consequence: no overloading by parameter

Because both erase to the same signature, you cannot overload a method on the type argument:

void f(List<String> s) {}
void f(List<Integer> i) {}   // COMPILE ERROR — same erasure: f(List)
type-checkeraseList<String>sourcejavacinsert castsListruntime class
List<Integer> travels the same path to the same runtime class, which is why a.getClass() == b.getClass() prints true. The type argument is spent at compile time; nothing of it survives.
ExpressionVerdictBecausenew T()compile errorno runtime class to makex instanceof List<String>compile errorno type argument leftx instanceof List<?>legalnothing to checknew T[10]compile errorarrays are reifiedf(List<String>) overloadcompile errorboth erase to f(List)Pass a Class<T> token when you need the type at runtime.
Every rejection has the same root cause: after erasure the JVM has no type argument to work from. The standard escape is a Class<T> token, which survives erasure as an ordinary object.
NORMAL ~/memra/learn/comp-308/type-erasure-and-consequences utf-8 LF