Type Erasure in Java Generics
Understand how Java generics disappear at compile time, what erasure does to your types, and the implications for your code.
Understand how Java generics disappear at compile time, what erasure does to your types, and the implications for your code.
Type Erasure in Java Generics
Type erasure is the mechanism by which Java implements generics. At compile time, all generic type information (<T>, <String>) is removed — replaced by their erased types (typically Object or the leftmost bound). The JVM has no knowledge of generics at runtime. This was a deliberate design choice made in Java 1.5 to maintain binary compatibility with pre-generics code.
Introduction
Type erasure means that generic type parameters (<T>, <K, V>, etc.) are a compile-time fiction. The bytecode that runs on the JVM has no knowledge of generics — all <T> becomes Object (or the leftmost bound type). This has profound implications: you cannot use T.class, new T(), instanceof T>, or arrays of T> directly in Java generics.
Understanding erasure is essential because it explains why certain operations fail at compile time, why bridge methods appear in bytecode, and why maintaining binary compatibility required this design. Erasure also means that code compiled with generics can interoperate with pre-generics code seamlessly — a List<String> and a raw List are the same class at runtime.
The practical impact: generics are a pure compile-time safety net. The runtime sees raw types. Any operation that relies on reified generics (storing type information in fields, runtime type checks, generic arrays) requires workarounds. This post covers what erasure actually does, the bridge methods the compiler generates, and the failure modes that arise from erasure in production code.
What Gets Erased
| Generic Declaration | Erasure |
|---|---|
<T> | Object |
<T extends Number> | Number |
<T extends Comparable<T>> | Comparable (the bound, after its own erasure) |
<K, V> (multiple) | Each erased independently per its own bound |
List<String> | List (raw type) |
List<? extends Number> | List (raw type, with wildcard flag for compiler) |
After erasure, the compiled bytecode is identical to pre-generics Java code that used Object casts throughout.
Code Example: Before and After Erasure
// SOURCE CODE
public class Container<T> {
private T value;
public T get() { return value; }
public void set(T value) { this.value = value; }
}
// COMPILED BYTECODE (erasure applied)
public class Container {
private Object value; // T replaced with Object
public Object get() { return value; }
public void set(Object value) { this.value = value; }
}
The same happens at call sites:
// SOURCE
Container<String> box = new Container<>();
box.set("Hello");
// COMPILED
Container box = new Container();
box.set("Hello"); // "Hello" is already an Object — no cast needed
// retrieval: String s = box.get();
// compiler inserts: String s = (String) box.get();
// Erasure + cast insertion = what the compiler emits
Mermaid Diagram: Erasure Process — Compile Time
flowchart TD
A["public class Box<T>\n{\n T value;\n T get() { return value; }\n}"] --> B["Type Checking"]
B --> C["Erasure"]
C --> D["public class Box\n{\n Object value;\n Object get() { return value; }\n}"]
D --> E["Insert Casts"]
E --> F["Bytecode: Box.java"]
Runtime
At runtime, the JVM loads the raw class with no knowledge that it was ever generic. Box<String> and Box<Integer> both compile to the same Box.class — the type argument lives in the class file’s metadata (the generic signature attribute), but the JVM never sees it. That metadata is read only by the compiler at link time. It does not affect runtime behavior.
This bites in a few places. instanceof List<String> is a compile error because the JVM only knows about List — there is no List<String> class to check against. List<String>.class is similarly illegal. When you need runtime type information, you must pass an explicit Class<T> token. String.class carries Class<String> which retains the type at runtime. The generic declaration itself does not.
Reflection makes this worse. Field.getType() on a field of type T returns Object after erasure. Field.getGenericType() returns the generic type from the source signature attribute, but that is metadata only — the JVM does not enforce it. Method.getParameterTypes() strips wildcards and type parameters, leaving only raw types. This is why Gson requires TypeToken objects: they read the generic signature attribute from the class file and reconstruct the full parameterized type at runtime.
A practical consequence: if two classes implement the same generic interface with different type arguments, they share the same interface at runtime. Node<String> and Node<Integer> both implement Node after erasure. There is no way to distinguish them via reflection or instanceof. The type argument exists only in the source code and the class file’s metadata store.
flowchart TD
G["Loading Box.class"]
G --> H["No generic signature retained\n— just raw Box class"]
Code Example: Bounded Type Erasure
// SOURCE
public class NumericBox<T extends Number> {
private T value;
public double compute() {
return value.doubleValue(); // calling Number's method
}
}
// ERASED
public class NumericBox {
private Number value; // T replaced with leftmost bound: Number
public double compute() {
return value.doubleValue(); // still valid — Number has doubleValue()
}
}
The bound Number is kept after erasure because it is the type that provides the methods you call.
Failure Scenarios
1. Cannot Instantiate T
The restriction against new T() in generic code is not a syntactic limitation that could be removed with a more powerful compiler — it is a fundamental consequence of how erasure works. When the compiler processes a generic class or method, it removes all type parameters and replaces them with their erased types before generating bytecode. By the time the JVM sees the code, T does not exist — it has been replaced by Object or by the leftmost bound type. There is no type information left in the compiled bytecode that represents T.
public <T> void createInstance(Class<T> clazz) {
// T t = new T(); // compile error: cannot do new T
// Erasure makes T = Object at runtime, so new Object() would be wrong type
T instance = clazz.getDeclaredConstructor().newInstance(); // workaround
}
new T() fails for a simple reason. Erasure replaces T with Object before bytecode generation. At runtime, there is no type token floating around — the JVM has no idea what T was originally supposed to be. Calling new Object() would give you an Object, not whatever the caller intended. The compiler cannot verify the type safety of new T() because the runtime type does not exist.
The standard workaround is passing a Class<T> token. The caller knows the actual type at the call site and passes MyClass.class. The method then uses clazz.getDeclaredConstructor().newInstance() to instantiate via reflection. This is how Spring, Hibernate, and most dependency injection containers build objects without knowing the concrete type at compile time. The tradeoff is that the wrong token produces a runtime exception instead of a compile-time error — you lose the safety net generics normally provide.
The deeper issue is that new T() would require the JVM to reify the type parameter at runtime — to actually know what T is when executing the bytecode. Java’s generics were specifically designed to avoid this requirement, which is why they use erasure rather than reification. Languages with reified generics (like C# or the Common Language Runtime) can call new T() because the generic type parameter is preserved at runtime. Java made the opposite choice to maintain binary compatibility with pre-generics code, and new T() is one of the direct casualties of that design decision.
Note that even with a Class<T> token, instantiation through reflection bypasses the compiler’s type checking. If you pass String.class but the method expects Class<Integer>, the compiler cannot catch the mismatch — Class<String> is assignable to Class<?> and the unchecked conversion happens silently. The result is a runtime ClassCastException when the cast inside newInstance() fails. This is the same erasure risk that applies to all raw type operations: the compiler steps aside, and type safety becomes the developer’s responsibility.
2. Cannot Create Generic Arrays Directly
Arrays in Java carry reified type information at runtime — the JVM knows the element type of an array and enforces store operations against that type. When you try to create new T[10] in generic code, the compiler faces a problem: after erasure, T becomes Object, so new T[10] would be new Object[10]. But an Object[] is not a String[] at runtime, even if the code that later retrieves elements expects String. The type information about T is gone from the bytecode entirely.
public <T> void wrongArray() {
// T[] arr = new T[10]; // compile error
// Fix: use Array.newInstance()
T[] arr = (T[]) Array.newInstance(Object.class, 10); // unchecked cast
}
Generic array creation runs into the same erasure problem. new T[10] would become new Object[10] at runtime, and the JVM cannot insert a type check when you pull an element back out. The array type itself carries no information about T, so retrieving arr[0] and assigning it to a String variable would be blind. The compiler refuses to generate that code.
The Array.newInstance() workaround creates an Object[] and casts it to T[]. This compiles, but the cast is unchecked — the compiler cannot verify that the array actually holds T instances. If you mix in a raw-type reference or use varargs, you get heap pollution: the array claims to be String[] but the runtime type is Object[]. The Class<T> token does not help here because arrays of generic types are inherently unsafe with varargs. @SafeVarargs silences the warning but does not fix the underlying type hole.
The fundamental issue is that Java arrays are covariant — String[] is a subtype of Object[] — and this covariance is enforced at runtime via the array’s type tag. A generic array would need to carry a type parameter through runtime, which requires reification that erasure explicitly avoids. The workaround using Array.newInstance(Object.class, size) and casting is the standard pattern in framework code (Spring, Guava, etc.), but it shifts type safety from compile time to runtime. Any code that retrieves elements from the array without an explicit cast will still get the right behavior, but raw array references or varargs parameters create opportunities for heap pollution that the JVM can only detect at the point of an incompatible store operation, throwing ArrayStoreException.
3. Collision After Erasure
Method overloading in Java requires distinct method signatures — the JVM uses the full parameter type signature to resolve which method to invoke at a call site. When two methods have the same name but different parameter types at the source level, the JVM distinguishes them by those parameter types. But type erasure removes type arguments, potentially causing two source-level distinct methods to collapse into the same JVM signature.
// Two methods with the same erasure — compile error
public class Colliding {
// public void set(T value) { }
// public void set(List<T> list) { } — both erase to set(Object)
}
Method signature collision is a direct consequence of erasure. When T erases to Object, every method that uses T in its parameter list ends up with Object as the parameter type after erasure. If you have two methods that both take T but in different contexts, they both produce the same erased signature. The JVM cannot tell them apart, so the compiler rejects the code before it even reaches bytecode.
The collision problem shows up in practice when mixing generic methods with overloaded methods that use different type wrappers. <T> void process(T item) and <T> void process(List<T> list) both erase to void process(Object) — a name clash. The fix is to use distinct erased types: bounded parameters like <T extends Number> and <T extends CharSequence> produce different erasures (Number vs CharSequence), so the methods can coexist. When bounds are the same or absent, you must refactor the signature to avoid the clash.
This collision detection is one of the places where the compiler acts as a safety net for a problem that would otherwise be extremely subtle. Without this check, two methods that compile fine separately would become ambiguous at runtime, and the JVM would pick one nondeterministically based on internal resolution rules. The compile-time error forces you to make the distinction explicit — either through bounds that produce different erasures, or by renaming one of the methods to clarify the distinction. This is one of the reasons the Checker Framework and similar tools recommend using explicit bounds wherever possible: they reduce the surface area for erasure-related signature collisions.
4. Cannot Call Class methods on T at Runtime
public <T> void example(T param) {
// Class<?> c = param.getClass(); // works — getClass() is on Object
// But Class<T> methods that need T at runtime are unsafe
// T.class cannot be used: Class<T> does not give you .class at runtime
}
param.getClass() actually works — every object has getClass(), so the call compiles fine. The result is a raw Class<?> pointing to the actual runtime class of the argument. That part is safe. What is not safe is expecting Class<T> to retain the type parameter at runtime. Class<String> is a reifiable type — the JVM knows it is Class<String> — but T inside a generic method is not reifiable. Erasure strips it before the method ever executes.
The practical impact shows up when you try to use T.class as a class literal. T.class is a syntax error because T does not exist at runtime. Class<T> as a parameter type only carries the type information if the caller passes it explicitly. If you call save(String.class), the Class<String> token enters the method and you can use it for newInstance() calls or cast(). But inside the generic method itself, there is no way to derive a Class<T> from T alone. This is why many generic frameworks require you to pass the class token as an explicit parameter rather than trying to extract it from the type parameter.
Trade-Off Table
| Concern | Impact of Erasure | Mitigation |
|---|---|---|
instanceof with generics | Illegal — no runtime type info | Use alternative: markers, separate methods |
new T() | Not possible | Use Class<T>.newInstance() or Array.newInstance() |
T.class | Not valid syntax | Pass Class<T> as parameter |
| Array creation | No direct new T[] | Use Array.newInstance() with unchecked cast |
| Bridge methods | Compiler adds to preserve overrides | Be aware when debugging bytecode |
| Binary compatibility | Pre-generics code works with generics code | No breaking changes to existing JARs |
Observability Checklist
- Use
javap -con compiled classes to see actual erased signatures and bridge methods - Check for “unchecked” warnings in compilation output — these signal erasure-related unsafe operations
- Verify framework serialization does not rely on generic type parameters (most handle erasure via TypeToken / custom serializers)
- Test with pre-generics code paths if maintaining binary compatibility with older JARs
- Use
javap -vfor full constant pool and generic attribute debugging
Security Notes
- No runtime type enforcement:
List<String>andList<Integer>are both justListat runtime. Malicious code that bypasses generics can inject the wrong type. Validate at boundaries. - Unsafe casts: Erasure means the compiler inserts casts. If you use raw types or
@SuppressWarnings("unchecked"), you bypass these safety nets. Audit for raw type usage in security-critical paths. - Reflection exposure: Reflection APIs like
Field.getGenericType()returnTypeobjects that include generic info from source, but the actual field in the class file is erased. Do not use generic type info for security decisions.
Pitfalls
-
Class<T>does not retain T at runtime:List<String>.classis illegal. You must pass aClass<String>token explicitly if you need reifiable type information at runtime. -
Varargs and heap pollution:
List<String>[]varargs can cause heap pollution because the array type is erased.@SafeVarargssuppresses the warning but does not fix the issue. -
Confusing errors when bounds are missing: If you call a method on
Tand it is not in the bound, the compile error references erasure — not the original type parameter. -
Generic enums with inheritance: Enum classes cannot extend other types, and generic enum declarations are not allowed because enums inherit
java.lang.Enumwhich is already parameterized.
Quick Recap
- Type erasure removes all generic type information at compile time
- Unbounded
<T>becomesObject; bounded<T extends X>becomesX - The compiler inserts casts at retrieval points and generates bridge methods where needed
- Runtime sees raw types only —
List<String>andList<Integer>are identical at runtime - Erasure is the reason you cannot
new T(), useT.class, orinstanceof List<String> - Erasure was chosen to maintain backward binary compatibility with Java 1.4 and earlier
Interview Questions
- >(){}` (anonymous subclass of TypeToken) or passing `Class
- >` directly. Some frameworks (like Spring) use `Class.forName()` with parameterized types to resolve generic type information at runtime via the generic signature attribute."
Further Reading
- Generic Classes — defining classes with type parameters
- Generic Methods — writing flexible methods with type parameters
- Wildcards — unbounded and bounded wildcard type arguments
- Type Bounds — upper and lower bounds on type parameters
- Bridge Methods — compiler-generated methods from type erasure
- Oracle: Type Erasure — official documentation on how generics are erased at compile time
- OpenJDK: Type Erasure Source — source code showing generic type information retention
Conclusion
Type erasure is the mechanism that makes Java generics work without changing the JVM. At compile time, all generic type information is removed — unbounded T becomes Object, bounded T extends Number becomes Number. The compiler also inserts the casts that would otherwise be written by hand. At runtime, there is no List<String> — only a raw List.
The consequences of erasure shape everything you cannot do with Java generics. No new T(), no T.class, no instanceof List<String>. These are not arbitrary restrictions — they are the natural result of erasing type information before the bytecode is loaded. The JVM simply does not know what T was.
What many miss is that erasure also affects bounds. <T extends Number & Comparable<T>> erases to Number — not Comparable. The secondary bound (Comparable<T>) is recorded in the class file’s generic signature attribute for the compiler’s use, but the field and method signatures use only the first bound’s type. This has real implications when crossing into reflection or dealing with complex generic inheritance chains.
Bridge methods are the direct consequence of erasure. When a subtype overrides a generic method with a more specific return type, the signatures no longer match after erasure. The bridge method restores the override relationship: Object get() delegates to String get(). See Erasure and Bridge Methods in Java for the full mechanics.
Category
Related Posts
Abstract Classes in Java
Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.
Arithmetic Operators in Java
Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.
Array Basics in Java
Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.