Type erasure & its consequences
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)