skip to content

How does type erasure handle an upper-bounded type parameter, and what runtime artifacts (like casts and bridge methods) does it produce?

level: principalimportance: nice to knowfreq 25%

answer

  1. erasure = compile-time only
  2. T erases to leftmost bound (Object if none)
  3. synthetic casts at consumption sites
  4. bridge methods restore overriding
  5. no overload/instanceof/new T[] by argument

basics

~20 s

At compile time Java erases the type parameter, replacing it with its upper bound (Object if none). The compiler then inserts casts where needed and may generate hidden bridge methods so overriding and polymorphism still work at runtime.

solid answer

~50 s

Java generics are a compile-time feature: after type checking, the compiler erases type parameters. An upper-bounded parameter is replaced by its bound — <T extends Number> erases T to Number, and an unbounded <T> erases to Object. Field and method signatures use the erased types in the bytecode. To preserve type safety, the compiler inserts synthetic casts at call sites where a generic value is consumed as a specific type. When a generic class implements or overrides a method whose erased signature differs from the supertype's, the compiler emits a bridge method: a synthetic method with the supertype's erased signature that delegates to the real one, so dynamic dispatch and the Liskov substitution still hold. Consequences for principals: you cannot overload on types that erase identically, you cannot do runtime instanceof on a parameterized type, arrays of generics are unsafe, and the leftmost bound drives the erased signature, which matters for binary compatibility.

code

java · 16 lines
java
class Box<T extends Number> {
    private T value;
    T get() { return value; }
}
// After erasure (conceptually):
// class Box {
//     private Number value;
//     Number get() { return value; }
// }

class Holder<T> { void set(T t) { } }
class StringHolder extends Holder<String> {
    @Override void set(String s) { }
    // compiler also generates a synthetic bridge:
    // void set(Object o) { set((String) o); }
}

go deeper

for a junior

Knows generics 'disappear' at runtime and that you can't do new T[]; may not know the mechanics.

for a middle

States that T erases to its bound (Object by default) and that runtime type-argument checks are impossible.

for a senior

Explains synthetic casts and bridge methods and the practical limits (overloading, instanceof, arrays).

for a principal

Reasons about erased signatures and the leftmost bound for binary compatibility and API evolution, and explains bridge-method behavior under reflection.

## What type erasure is Java implemented generics without changing the JVM: generics exist only in source and during compilation. After the compiler type-checks your code, it **erases** the generic type information — a process called **type erasure** — producing bytecode that looks much like pre-generics Java. This keeps old `.class` files and new ones interoperable (**migration compatibility**). ## How an upper bound is erased The erasure of a type parameter is **its leftmost bound**, or `Object` if it has none: - `<T>` (i.e. `<T extends Object>`) erases `T` to `Object`. - `<T extends Number>` erases `T` to `Number`. - `<T extends Number & Comparable<T>>` erases `T` to `Number` (the first bound). So a field of type `T` becomes a field of the bound type, and a method `T get()` becomes `Number get()` (or `Object get()`). ```java class Box<T extends Number> { private T value; T get() { return value; } } // erases to roughly: class Box { private Number value; Number get() { return value; } } ``` ## Synthetic casts Because the bytecode now traffics in the erased (bound) type, the compiler inserts **synthetic casts** at the places where a caller uses the value as a more specific type. If you call `Integer i = intBox.get();` on a `Box<Integer>`, the compiler emits a checked cast to `Integer`. These casts are why a malformed generic usage can throw a `ClassCastException` at a surprising line — the cast is invisible in source. (With a `Number` bound, `get()` already returns `Number`, so a cast appears only when narrowing further.) ## Bridge methods Erasure can make an overriding method's signature differ from the one it overrides. Consider: ```java class Holder<T> { void set(T t) { } } class StringHolder extends Holder<String> { @Override void set(String s) { } } ``` After erasure, `Holder.set` has signature `set(Object)`, but `StringHolder.set` has `set(String)`. These are different descriptors, so `StringHolder` would not actually override `Holder.set(Object)` — polymorphism through a `Holder` reference would break. To fix this, the compiler generates a **bridge method** in `StringHolder`: ```java // synthetic, compiler-generated void set(Object o) { set((String) o); } ``` This bridge has the supertype's erased signature and forwards to the real method, restoring correct **dynamic dispatch** and the Liskov substitution principle. Bridge methods are marked synthetic and bridge in the bytecode; they occasionally surface in reflection or stack traces. ## Downstream consequences a principal must know 1. **No overload by erasure-equal signatures.** `void f(List<String>)` and `void f(List<Integer>)` both erase to `f(List)` — a compile error ("name clash"). 2. **No runtime parameterized-type checks.** `x instanceof List<String>` is illegal; only `x instanceof List<?>` works, because the type argument is gone at runtime. 3. **Generic array creation is forbidden.** `new T[10]` and `new List<String>[10]` are disallowed because the array's runtime component type can't be reified; you work around with `Object[]` + casts or `Array.newInstance` plus the bound. 4. **The leftmost bound is binary-compatibility-sensitive.** Changing the first bound changes the erased signature, which can break already-compiled callers — relevant when evolving a public library. 5. **Reflection sees bounds, not arguments.** `getGenericSuperclass`/`getGenericInterfaces` can recover declared type arguments via the class file's signature attribute, but a plain runtime object carries only its erased type. ## Why bound-aware erasure is useful Bounding to `Number` instead of leaving `Object` means the erased signature is `Number`, so callers and overriders interact with a more specific, more useful type, and fewer synthetic casts are needed. The bound is therefore not just a source-level convenience — it shapes the generated bytecode.

  • Why does the compiler generate a bridge method when a generic supertype is overridden?
    Because erasure can give the override a different descriptor than the supertype method; the synthetic bridge has the supertype's erased signature and delegates to the real method, restoring correct dynamic dispatch.
  • What does <T extends Number> erase T to, and why does that matter?
    To Number — the leftmost bound. It makes erased signatures use Number instead of Object, reducing synthetic casts, but changing the first bound is a binary-compatibility hazard for compiled callers.
  • Why is new T[10] illegal for a type parameter?
    Arrays are reified (they know their component type at runtime), but T is erased, so the runtime component type is unavailable; you use Object[] with casts or Array.newInstance with the bound's class.

saying these in an interview costs you the question

  • Believing type arguments exist at runtime (they are erased)
  • Thinking you can overload methods that differ only by type argument
  • Trying instanceof against a parameterized type like List<String>
  • Forgetting bridge methods explain synthetic entries in stack traces/reflection
  • Assuming new T[] works for a bounded T

context