Generics

Type Erasure

Generics are a compile-time construct: after compilation, Pair<String> and Pair<Integer> are the same class Pair, with type variables replaced by their bounds and casts inserted at use sites. Every generics limitation traces back to this.
  • One class file per generic type — type arguments vanish at runtime
  • T erases to its first bound (Object if unbounded)
  • The compiler inserts synthetic casts at reads and bridge methods for inheritance
  • No runtime type argument info: instanceof List<String> is illegal
  • Erasure bought seamless migration compatibility with pre-generics code
What the JVM actually sees
// You write:
Pair<Employee> buddies = new Pair<>(e1, e2);
Employee first = buddies.getFirst();

// After erasure the JVM sees:
Pair buddies = new Pair(e1, e2);          // raw Pair — T became Object
Employee first = (Employee) buddies.getFirst();   // compiler-inserted cast

Java chose erasure so generic code could interoperate with a decade of existing libraries without recompilation — unlike C++ templates (a class per instantiation) or C# reified generics (runtime type info). The price: the runtime cannot distinguish List<String> from List<Integer>; both are just List.

Bridge methods

Why bridges exist
class DateInterval extends Pair<LocalDate> {
    @Override
    public LocalDate getSecond() { ... }        // your override
}
// Erased Pair has: Object getSecond()
// The compiler synthesizes a bridge in DateInterval:
//   Object getSecond() { return getSecond(); }  // calls YOUR LocalDate version
// — preserving polymorphism across the erased signature.

Bridges surface in stack traces and reflection (Method.isBridge()). They also implement covariant return types for non-generic code — the same mechanism.

Sources
  • Core Java, Volume I: Fundamentals (13th ed.)Ch. 8.5 — Generic Code and the Virtual Machine
  • Effective Java (3rd ed.)Item 33