Skip to content
Part IA Michaelmas Term

Type Erasure: Java's Implementation

Compile-time vs runtime generic types

Two Approaches to Generics

C++ Templates (Code Generation)

The compiler generates a separate specialised class for each distinct set of type arguments. If your code uses Box<String>, Box<Integer>, and Box<Person>, the compiler produces three independent classes — Box_String, Box_Integer, Box_Person — each with the type parameter replaced by the concrete type.

Advantages: maximal runtime efficiency (no casts, no boxing, the generated code is as if you had hand-written a StringBox class). Each instantiation can be independently optimised.

Disadvantages: code bloat (the object code grows with each unique instantiation). The source code of the template must be available at every point of instantiation (header-only libraries in C++). Compilation is slower.

Java Type Erasure (Compile-Time Only)

The compiler uses generic type information for static type checking only. After checking, it erases the type parameters:

  • T without a bound is replaced by Object.
  • T extends Number is replaced by Number.
  • The compiler inserts casts automatically at every point where a generic value is retrieved.

At runtime, List<String> and List<Integer> are the identical class — List:

List<String> strings = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
System.out.println(strings.getClass() == numbers.getClass());  // true

There is no way to recover T at runtime via reflection — the String and Integer type arguments have been erased from the class.

How Erasure Works Step by Step

Consider this generic class:

class Box<T> {
    private T value;

    void set(T value) { this.value = value; }
    T get() { return value; }
}

After erasure, the JVM sees:

class Box {
    private Object value;

    void set(Object value) { this.value = value; }
    Object get() { return value; }
}

And the call site:

Box<String> box = new Box<>();
box.set("hello");
String s = box.get();

After erasure, the compiler inserts a cast:

Box box = new Box();
box.set("hello");
String s = (String) box.get();  // cast inserted by compiler

The cast is guaranteed safe because the compiler has already verified that only String objects can enter this box.

Why Java Chose Erasure

The decision was driven by backward compatibility. Java 5 generics had to work with:

  1. Existing bytecode — libraries compiled before Java 5 (with raw types) must interoperate with new generic code, and vice versa.
  2. The existing JVM — the JVM specification should not need to change to support generics. No new bytecodes were added.
  3. Billions of lines of existing Java code — all that code should still compile and run correctly.

Type erasure achieves all three goals: generic code compiles to standard JVM bytecode indistinguishable from pre-generics code. A List<String> is a List at the bytecode level, and a method compiled with raw List can be called from generic code (with a warning) and vice versa.

Raw Types

A raw type is a generic class used without type parameters:

List rawList = new ArrayList();   // raw type — no type argument
rawList.add("hello");
rawList.add(42);                   // no compile error — raw types are unchecked

Raw types exist solely for backward compatibility with pre-generics code. The compiler issues an unchecked warning when you use them. All generic type checks are disabled for raw types — they behave exactly as pre-generics code did.

The instanceof Limitation

Because type parameters are erased, you cannot check them at runtime:

if (obj instanceof List<String>) { ... }   // COMPILE ERROR
if (obj instanceof List) { ... }            // OK — the raw List is checkable

The JVM has no record of the String parameter — it only knows whether obj is some kind of List.

Summary: Trade-off

Type erasure was a pragmatic engineering decision. It got generics into the Java ecosystem without breaking the existing world — every line of pre-5 Java code, every compiled .class file, every JVM installation — at the cost of some runtime expressiveness. C++ took the opposite approach (code generation), gaining runtime expressiveness at the cost of backward compatibility and binary complexity.