Type erasure in Java
Generic types are erased at runtime; bridge methods.
A compile-time costume
Generics live only at compile time. The compiler checks your types, inserts casts, then erases the type arguments. In the bytecode, List<String> and List<Integer> are both just List. This is type erasure.
// you write:
String s = names.get(0);
// bytecode effectively does:
String s = (String) names.get(0);Your turn
What does this print?
var a = new ArrayList<String>();
var b = new ArrayList<LocalDate>();
boolean same = a.getClass() == b.getClass();
System.out.println(same);
System.out.println(a.getClass().getName());false java.util.ArrayList<String>true java.util.ArrayListfalse java.util.ArrayList
Show the answer
There's only one ArrayList class at runtime. Type arguments aren't part of the class object, so they're equal and the name is simply java.util.ArrayList.
What T turns into
An unbounded **T erases to Object. A bounded T erases to its leftmost bound**: <T extends Comparable<T>> becomes Comparable, and <T extends Number & Comparable<T>> becomes Number.
<T> // T → Object
<T extends Comparable<T>> // T → Comparable
<T extends Number & Comparable<T>>
// T → NumberOverloads that erase to the same thing
These look like two different methods, but after erasure **both are print(List): the same signature. The compiler reports a name clash**. Give them different names instead.
class Printer {
void print(List<String> s) {}
void print(List<Integer> n) {} // ✗ clash
}Bridge methods
Erasure creates a puzzle: Comparable<Tag> erases to compareTo(Object), but you wrote compareTo(Tag). So the compiler quietly adds a synthetic bridge method compareTo(Object) that casts and forwards to yours. Old erased callers keep working.
// you write:
public int compareTo(Tag o) { ... }
// compiler adds a bridge:
public int compareTo(Object o) {
return compareTo((Tag) o);
}Count the methods
Reflection lists every method the class really has. What does this print?
record Tag(String s)
implements Supplier<String> {
public String get() { return s; }
}
void main() {
int n = 0;
for (var m : Tag.class.getDeclaredMethods())
if (m.getName().equals("get")) n++;
System.out.println(n);
}120
Show the answer
You wrote one get() returning String, but the erased Supplier declares **Object get(). The compiler adds a bridge** Object get() that calls yours, so reflection finds 2.
Where erasure shows up
Because List<User> is just List at runtime, JSON libraries can't see the element type on their own: Jackson needs a TypeReference<List<User>>, and many frameworks ask for a Class<T> argument. You'll also spot bridge methods in stack traces now and then.
Key takeaways
- At runtime, a List<String> is just a List
- T erases to its leftmost bound, or Object
- Two overloads that erase to the same signature clash
- Bridge methods connect erased signatures to your typed overrides
Erasure was chosen for "migration compatibility": generic Java 5 code could call old pre-generics libraries and vice versa, without recompiling the world. C#, by contrast, keeps generic types at runtime.
Practice questions
What does this print?
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass());
System.out.println(a.getClass().getName());- false java.util.ArrayList<String>
- true java.util.ArrayList
- false java.util.ArrayList
- true java.util.ArrayList<String>
Check your answer
true java.util.ArrayList. There is only one ArrayList class at runtime. Type arguments don't exist in the class object, so the name is simply java.util.ArrayList.
What does this print?
class Printer {
void print(List<String> s) {}
void print(List<Integer> n) {}
}
void main() {
System.out.println("ok");
}- ok
- Compile error
- Throws IllegalStateException
- Prints nothing
Check your answer
Compile error. After erasure both methods would be print(List) — the same signature. The compiler reports a name clash.