🧪 Generics · Intermediate

Type erasure in Java

Generic types are erased at runtime; bridge methods.

🧩 The mysteryAt runtime, a List<String> and a List<Integer> are the exact same class. Your type arguments simply vanish after compiling. So who's checking anything?

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);
🔮 Predict it

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());
  1. false java.util.ArrayList<String>
  2. true java.util.ArrayList
  3. false 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 → Number
⚠️ The trap

Overloads 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);
}
🔮 Predict it

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);
}
  1. 1
  2. 2
  3. 0
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.

💼 In the real world

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

  1. At runtime, a List<String> is just a List
  2. T erases to its leftmost bound, or Object
  3. Two overloads that erase to the same signature clash
  4. Bridge methods connect erased signatures to your typed overrides
🤯 Did you know?

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());
  1. false java.util.ArrayList<String>
  2. true java.util.ArrayList
  3. false java.util.ArrayList
  4. 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");
}
  1. ok
  2. Compile error
  3. Throws IllegalStateException
  4. Prints nothing
Check your answer

Compile error. After erasure both methods would be print(List) — the same signature. The compiler reports a name clash.

Next: erasure has consequences. Why can't you write new T(), or even new T[10]?