What Is Type Erasure in Java? Bridge Methods and Limits
Type erasure strips Java generic type arguments after compiling: a List of String is a plain List at run time. Why new T fails, bridge methods, heap pollution.
- Course: Java study plan
- Module: Generics
- Kind: Lesson
- Reading time: 14 min
- Runtime: OpenJDK 18
What is type erasure in Java?
Type erasure is how Java implements generics: the compiler checks every generic use, then replaces each type parameter with Object or its first bound and inserts casts where values are read. At run time List<String> and List<Integer> are the same List class. Erasure kept older code and JVMs compatible, and it is why new T() and new T[n] are illegal.
Lesson
Everything the compiler knows about List<String> it forgets before the class file is written. That decision — erasure — is why Java generics are backward compatible, and why a whole list of natural-looking things are impossible: new T(), T[], instanceof List<String>, overloading on type arguments, a static field of type T. This lesson explains what erasure does, walks through each limitation with the reason, and covers the two artefacts it leaves behind — bridge methods and heap pollution.
What erasure does
The compiler:
- Checks every generic use for type correctness.
- Replaces each type parameter by its erasure:
Objectfor an unboundedT, the first bound for a bounded one (<T extends Number>→Number). - Inserts casts where generic code returns a
Tto typed code:String s = list.get(0)becomesString s = (String) list.get(0). - Generates bridge methods where erasure would break overriding (below).
After that, List<String>, List<Integer> and raw List are the same class at run time. A JVM from 2004 can run today's generic code.
new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass() // true
List<String> a = …; a.getClass().getName() // "java.util.ArrayList" — no <String>
Contrast C#, whose generics are reified: List<string> and List<int> are distinct run-time types. Java chose compatibility; the limitations below are the cost.
What you cannot do, and why
1. Instantiate a type parameter
T t = new T(); // error: unexpected type
T is Object at run time; there is no class to instantiate. Fix: pass a Supplier<T> (factory.get()) or a Class<T> token (clazz.getDeclaredConstructor().newInstance()).
2. Create a generic array
T[] arr = new T[10]; // error: generic array creation
List<String>[] lists = new List<String>[3]; // error too
Arrays check their element type at run time (ArrayStoreException); a T[] would have no type to check against. Fix: (T[]) new Object[10] with a contained @SuppressWarnings("unchecked") (what ArrayList does), or Array.newInstance(clazz, 10) with a class token, or use a List<T>.
3. Test a type argument at run time
if (obj instanceof List<String>) … // error: illegal generic type for instanceof
if (obj instanceof List<?>) … // fine — the raw class is all that exists
List<String> l = (List<String>) obj; // compiles with an UNCHECKED warning: the cast checks only "is it a List"
The JVM cannot see the String; the cast is a promise, not a check. If the promise is wrong, the ClassCastException appears later, at the first String s = l.get(0).
4. Use a type parameter in a static context
class Box<T> { static T shared; } // error
One class serves every T; a static field would have to be all of them at once.
5. Overload on type arguments
void process(List<String> xs) { }
void process(List<Integer> xs) { } // error: name clash — both erase to process(List)
Same erasure, same signature. Different method names, or one generic method.
6. Catch or throw a generic exception
class MyException<T> extends Exception { } // error: a generic class may not extend Throwable
catch clauses are resolved by run-time class; a parameterised exception could not be distinguished.
7. Primitives as type arguments — List<int>: nothing to erase to. Wrappers.
Bridge methods
class Node<T> { T value; void set(T v) { value = v; } }
class IntNode extends Node<Integer> {
@Override void set(Integer v) { … } // overrides set(T)? After erasure the parent has set(Object)
}
set(Integer) does not have the same erased signature as set(Object), so the compiler generates a hidden bridge set(Object v) { set((Integer) v); } in IntNode so overriding works through the erased parent. You see bridges in stack traces and reflection (Method.isBridge()); you never write them. They are also why a ClassCastException can appear inside a bridge when heap pollution (next) delivers the wrong type.
Heap pollution
A variable of a parameterised type refers to an object that is not of that type:
List<String> strings = new ArrayList<>();
List raw = strings;
raw.add(42); // warning: unchecked call — the String list now holds an Integer
String s = strings.get(0); // ClassCastException here, with no cast visible in the source
Raw types, unchecked casts and generic varargs are the three ways to cause it. Every "unchecked" warning marks a place where erasure lets a lie through; treat them as errors, and confine the unavoidable ones ((T[]) new Object[n]) to the smallest scope with @SuppressWarnings("unchecked") and a comment.
Reifiable types
A type whose full information survives erasure is reifiable: primitives, non-generic classes, raw types, List<?> (unbounded wildcard), arrays of reifiable types. Only reifiable types may appear in instanceof, array creation and catch. Everything with a concrete type argument is non-reifiable.
Type tokens: carrying the type at run time
static <T> T fromJson(String json, Class<T> type) { … type.cast(parsed) … }
User u = fromJson(text, User.class);
static <T> T[] newArray(Class<T> type, int n) { return (T[]) Array.newInstance(type, n); }
A Class<T> parameter reintroduces the type at run time: it can create instances, arrays, and checked casts (type.cast(obj) throws a real ClassCastException immediately). Libraries that need List<User> (not just User) use a super type token — an anonymous subclass of a generic class whose superclass type argument is retained by reflection (new TypeReference<List<User>>() {} in Jackson) — the one place generic information does survive.
Reflection and generic signatures
Erasure removes type arguments from bytecode instructions, but declarations keep a Signature attribute: Field.getGenericType() returns List<String> for a field declared that way, and Class.getGenericSuperclass() is what super type tokens read. Declarations remember; values do not.
Interview angle
- "What is type erasure?" The compiler replaces type parameters with their bounds and inserts casts; the JVM sees raw types.
- "Why can't you write
new T()?" At run timeTisObject; there is no class to construct. Use aSupplier<T>orClass<T>. - "Why can't you overload
f(List<String>)andf(List<Integer>)?" Both erase tof(List). - "What is a bridge method?" A compiler-generated method that lets an override with a specialised signature satisfy the erased parent signature.
- "What is heap pollution?" A parameterised variable holding an object of the wrong type argument, via raw types or unchecked casts.
Key takeaways
- Erasure: check at compile time, erase to
Object/bound, insert casts.List<String>isListat run time. - Consequences: no
new T(), noT[], noinstanceof List<String>, no staticT, no overloading on type arguments, no generic exceptions, no primitives. - Bridge methods keep overriding working; heap pollution is the lie unchecked warnings point at.
- Class tokens (
Class<T>) and super type tokens carry type information into run time when you need it.
Common questions
Why can't you create an instance of a type parameter with new T()?
At run time T has been erased to Object, so there is no class to instantiate. Pass the knowledge in instead: a Supplier<T> and call factory.get(), or a Class<T> token and call getDeclaredConstructor().newInstance().
Why can't you overload methods on generic type arguments?
Because both erase to the same signature. process(List<String>) and process(List<Integer>) both become process(List), and the compiler reports a name clash. Give the methods different names, or write one generic method.
What is a bridge method in Java?
A bridge method is a hidden method the compiler generates so an override with a specialised signature still overrides the erased parent. If IntNode extends Node<Integer> overrides set(Integer), the compiler adds a set(Object) that casts and calls it; Method.isBridge() reveals them.
What is heap pollution in Java?
Heap pollution is a variable of a parameterised type referring to an object that is not of that type, such as a List<String> holding an Integer after a raw-type add. Raw types, unchecked casts and generic varargs cause it, and it surfaces later as a ClassCastException where no cast is visible.
Why is instanceof List<String> illegal in Java?
The JVM cannot see the String: after erasure only the raw List class exists, so the test could not be carried out. instanceof List<?> is allowed, and a cast to List<String> compiles only with an unchecked warning, checking nothing beyond "is it a List".
Exercises
Class tokens beat erasure
Write static <T> T[] filled(Class<T> type, int n, Supplier<T> factory) that creates a real T[] with java.lang.reflect.Array.newInstance and fills it from the supplier — the two things erasure forbids directly (new T[n], new T()). Then write static <T> int countOfType(Object[] items, Class<T> type) using type.isInstance.
Read an integer n. Build a String[] of n elements "s<index>" and an Integer[] of n squares with filled; print both with Arrays.toString, then strings=<count of String in a mixed Object[] made of both arrays> and array type=<the String[]'s getClass().getSimpleName()>.
Example: 3 →
[s0, s1, s2]
[0, 1, 4]
strings=3
array type=String[]Erasure, demonstrated
Read n words. Show what erasure means at run time: print sameClass=<new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass()>, className=<that class's simple name>. Then pollute the heap: put the words in a List<String>, assign it to a raw List, add(42) through the raw reference, and read the elements back through the typed reference one by one — print each string, and when the ClassCastException fires print heap pollution at index <i>. Finally print size=<list size>: the integer is really in there.
Example: 2 then a b →
sameClass=true
className=ArrayList
a
b
heap pollution at index 2
size=3In this module: Generics
← Wildcards and PECS · Generics in practice — idioms and error messages →