skip to content
everything.generics.typeErasure

type erasure

what the compiler deletes, what it adds (bridge methods), and why.

chapter
chapter 05.05
reading time
8 min read

generics live only in the compiler. javac checks every type argument, then erases them: T becomes its bound (Object if there isn't one), List<String> becomes plain List. to keep things working it inserts casts where values come back out, and writes bridge methods where erasure would break overriding.

the JVM never sees a type argument. that one decision explains almost every weird rule in this part.

the picture

you know how ArrayList<String> and ArrayList<Integer> feel like two different classes? at run time they're the same class, the same object layout, the same bytecode. the difference was only ever a promise the compiler checked for you.

think of an editor proofreading a manuscript with a red pen (javac). they check every sentence against the style guide, fix what's wrong, and then the red notes are thrown away before printing. the printed book (the class file) contains the result of the checking, never the checks themselves. a reader (the JVM) can't tell which sentences were reviewed.

hover a stage.

what javac does to generic code, in order. hover a stage. none of it costs anything at run time except the casts, and those are the ones you would have written by hand anyway.

what javac actually writes

talk is cheap. here's real javap output from javac 25, next to the source it came from:

show =
look at
Main.java
  1. 1class Main {
  2. 2 public static void main(String[] args) {
  3. 3 Box<String> box = new Box<>("hi");
  4. 4 String s = box.get();
  5. 5 System.out.println(s.length());
  6. 6 }
  7. 7}
$javap -c Main.class
public static void main(java.lang.String[]);
Code:
0: new #7 // class Box
3: dup
4: ldc #9 // String hi
6: invokespecial #11 // Method Box."<init>":(Ljava/lang/Object;)V
9: astore_1
10: aload_1
11: invokevirtual #14 // Method Box.get:()Ljava/lang/Object;
14: checkcast #18 // class java/lang/String
17: astore_2
18: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream;
21: aload_2
22: invokevirtual #26 // Method java/lang/String.length:()I
25: invokevirtual #30 // Method java/io/PrintStream.println:(I)V
28: return

get hands back an Object (that's its descriptor), and the very next instruction is a checkcast to String. that's the cast you didn't write. javac proved it can't fail, so it's free to insert it.

pick an example, then something to look at. the left is java, the right is the bytecode javac produced, with the matching lines in amber. watch for Object where you wrote T, and the checkcast nobody typed.

the rules, all of them:

in the sourceafter erasure
TObject
T extends NumberNumber
T extends Comparable<T> & SerializableComparable (the first bound)
List<String>, Map<K, List<V>>List, Map
T[]Object[]
String s = list.get(0)String s = (String) list.get(0)

notice that javap still prints class Box<T> and T get(). that's not a contradiction: javac also stores the generic signatures in the class file as metadata (a Signature attribute), for the compiler and for reflection. the descriptor, the thing the JVM links and runs against, is the erased one. what that metadata is good for is in generics at runtime.

predict the output
java
var a = new ArrayList<String>();
var b = new ArrayList<Integer>();
IO.println(a.getClass() == b.getClass());

bridge methods

erasure has one sneaky side effect. Comparable<T> declares compareTo(T), which erases to compareTo(Object). your Name implements Comparable<Name> declares compareTo(Name). in the bytecode those are different signatures, so yours doesn't override the interface method at all, and code that only knows it has a Comparable (like Collections.sort) would call a method that doesn't exist.

javac's fix is the bridge method: a synthetic compareTo(Object) that casts its argument to Name and calls yours. it's in the "a bridge method" tab above. reflection can see it too:

Main.java
import java.lang.reflect.Method;
import java.util.*;
 
record Name(String value) implements Comparable<Name> {
    public int compareTo(Name other) {
        return value.compareTo(other.value);
    }
}
 
public class Main {
    public static void main(String[] args) {
        Method[] ms = Name.class.getDeclaredMethods();
        Arrays.sort(ms, Comparator.comparing(Method::toString));
        for (Method m : ms)
            if (m.getName().equals("compareTo"))
                System.out.println(m + (m.isBridge() ? "   <- bridge, synthetic: " + m.isSynthetic() : ""));
        System.out.println(new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass());
    }
}
output, from jdk 25
public int Name.compareTo(Name)
public int Name.compareTo(java.lang.Object)   <- bridge, synthetic: true
true

this one needs a real JVM (reflection), so it doesn't run in the browser. copy it into java Main.java to try it.

bridges also show up for covariant return types, generic or not (override Object clone() with Point clone() and javac adds an Object clone() bridge). you almost never notice them, except in stack traces: a frame for a method on a line with no code, or a ClassCastException thrown from a line that's just the class declaration. that's the bridge's cast failing, because something called it through a raw type with the wrong argument.

the rules erasure forces on you

if the JVM doesn't know type arguments, anything that would need them at run time is off the table. javac tells you up front:

don'tasking for type arguments at run time
java
if (o instanceof List<String>) { }
// error: Object cannot be safely
//   cast to List<String>
doasking for what the object really has
java
if (o instanceof List<?> list) {
    // a List of something. check
    // the elements one by one if
    // you need to
}

instanceof List<String> would need the object to remember String. it doesn't, so the check is impossible and javac refuses (since java 16 it's allowed when the compiler can prove it's safe anyway, like a Collection<String> tested for List<String>). the same logic kills overloads that differ only in type arguments:

java
void print(List<String> xs) { }
void print(List<Integer> xs) { }
text
error: name clash: print(List<Integer>) and print(List<String>) have the same erasure

after erasure both are print(List), and a class can't have two methods with the same signature. the rest of the list, new T(), new T[], List<int>, generic exceptions, is in arrays, varargs and other limits.

why do it this way?

because in 2004 there were millions of lines of java using raw collections, compiled against libraries that were about to become generic. erasure meant:

  • the JVM didn't change. no new bytecodes, no new object layout, no type arguments to track at run time.
  • migration could happen in any order. a library could add type parameters without breaking a single compiled client, because List<String> and the old List compile to the same descriptor. clients could generify before or after their libraries did.
  • one copy of the code. ArrayList is one class however many type arguments you use, unlike templates that stamp out a copy per type.

C# made the opposite choice a year later: generics in .NET 2.0 are reified (the runtime knows List<string> from List<int>), which needed a new runtime and a parallel set of generic collection classes. java kept compatibility and paid with everything in the next chapter.

one level deeper:will java ever reify generics?+

not wholesale: it would break the migration story that erasure was chosen for. but project valhalla is working on the biggest limit, primitives. value classes (JEP 401, targeted as a preview for jdk 28) are the first step, and the longer-term plan is to let generic code work over value types without boxing, so a List<int>-like thing stops being a fantasy. what's coming is in valhalla.

keylines
  • javac checks generics, then erases them. the JVM never sees a type argument.
  • T becomes its first bound, List<String> becomes List.
  • javac inserts a cast wherever an erased value is read as something specific.
  • bridge methods keep overriding working after erasure.
  • no instanceof List<String>, no overloads that differ only in type arguments.
  • the reason: old code and new code had to run side by side, unchanged.