Generics

Type Erasure

15 min Lesson 7 of 13

Type Erasure

Generics in Java are a compile-time feature. Once the compiler has checked that your code is type-safe, it removes all generic type information from the bytecode. This process is called type erasure. Understanding erasure explains many "surprising" generic behaviours — from runtime instanceof limits to the existence of bridge methods.

What Actually Happens at Compile Time

When you write a generic class the compiler does two things: it checks types, then it rewrites the bytecode using the raw type. If a type parameter is unbounded, it is replaced by Object; if it has a bound (e.g. T extends Comparable<T>) it is replaced by the leftmost bound.

// Source code you write public class Box<T> { private T value; public Box(T value) { this.value = value; } public T getValue() { return value; } } // What the compiler emits (raw type — T erased to Object) public class Box { private Object value; public Box(Object value) { this.value = value; } public Object getValue() { return value; } }

The compiler also inserts unchecked casts at every call site where you read a typed value back out, so the cast happens once, in your code, not deep inside the library.

Box<String> box = new Box<>("hello"); String s = box.getValue(); // compiled as: String s = (String) box.getValue();
Key insight: Box<String> and Box<Integer> are the same class at runtime. There is only one .class file — Box.class — and both instances are instances of it.

Consequences: What You Cannot Do at Runtime

Because type parameters vanish from bytecode, you cannot use them in ways that require runtime type information:

  • Cannot create generic arrays: new T[10] is illegal because the JVM does not know what T is at runtime.
  • Cannot use instanceof with a type parameter: obj instanceof T will not compile.
  • Cannot do new T(): the JVM would not know which constructor to call.
  • Generic type parameters are not reified — you cannot ask, for example, List<String>.class; that expression does not exist. Only List.class does.
// Compile error: cannot create a generic array // T[] arr = new T[10]; // Workaround: pass the class token and use Array.newInstance public static <T> T[] makeArray(Class<T> type, int size) { @SuppressWarnings("unchecked") T[] arr = (T[]) java.lang.reflect.Array.newInstance(type, size); return arr; }

Bridge Methods

Erasure creates a subtle problem for inheritance. Consider a generic interface:

public interface Processor<T> { void process(T item); } // After erasure the interface becomes: // void process(Object item);

Now you write a concrete implementation:

public class StringProcessor implements Processor<String> { @Override public void process(String item) { System.out.println(item.toUpperCase()); } }

The method you wrote has the signature process(String), but the erased interface demands process(Object). These are different signatures — polymorphism would break. To fix this, the compiler automatically generates a bridge method:

// Synthetic bridge method generated by the compiler (you never write this) public void process(Object item) { process((String) item); // delegates to your real method }

You can see bridge methods in the bytecode with javap -v StringProcessor — they appear with the ACC_BRIDGE and ACC_SYNTHETIC flags. They are invisible in source code but are real methods in the .class file.

Why does this matter? If you use reflection to list the methods of a class, bridge methods show up. Always filter them out with method.isBridge() if you are building a framework or annotation processor.

Heap Pollution and Unchecked Warnings

Heap pollution occurs when a variable of a parameterised type refers to an object that is not of that type. It can happen when you mix raw types with generics, or when you use @SuppressWarnings("unchecked") recklessly.

List<String> strings = new ArrayList<>(); List rawList = strings; // raw type — no warning here rawList.add(42); // inserts an Integer — compiler warns String s = strings.get(0); // ClassCastException at runtime!

The cast the compiler inserted fails at the get call, not where the 42 was added — making the source of the bug harder to find. This is why you should never mix raw types and parameterised types in new code.

Treat every unchecked warning seriously. The compiler is telling you that erasure means it cannot guarantee type safety at that point. If you suppress the warning, add a comment explaining why the cast is actually safe.

Bounded Erasure

When a type parameter has a bound, the erased type is the bound, not Object:

// Source public <T extends Comparable<T>> T max(T a, T b) { return a.compareTo(b) >= 0 ? a : b; } // After erasure (T replaced by Comparable) public Comparable max(Comparable a, Comparable b) { return a.compareTo(b) >= 0 ? a : b; }

This is important because it means the compiler can verify that methods on the bound — like compareTo — are valid calls even after erasure.

Summary

Type erasure is Java's backward-compatibility solution: generics were added in Java 5 without changing the JVM, so all generic information exists only in the compiler. The runtime sees raw types; the compiler inserts casts and generates bridge methods to keep everything working. Understanding erasure prevents runtime surprises and helps you read error messages and reflection output with confidence.