skip to content

What is type erasure in Java generics, and what does the compiler do to the type parameters?

level: juniorimportance: must knowfreq 78%

answer

  1. Compiler checks types, then deletes them
  2. Unbounded → Object, bounded → leftmost bound
  3. Raw type left at runtime (List, not List<String>)
  4. Synthetic casts inserted at read sites
  5. Backward compatibility + no runtime cost

basics

~20 s

Type erasure means the generic type information (like the String in List<String>) is removed during compilation. At runtime, a List<String> and a List<Integer> are both just List. The compiler uses the types only for checking and then drops them.

solid answer

~40 s

Type erasure is how Java implements generics without changing the bytecode/JVM. During compilation the compiler checks all the type arguments for safety, then erases them: every type parameter is replaced in the class and method signatures. An unbounded parameter (like T in List<T>) becomes Object; a bounded one (T extends Number) becomes its leftmost bound (Number). Because the information is gone at runtime, generics are essentially a compile-time-only feature with no runtime overhead and no extra classes generated per type argument. To keep things type-safe despite the erasure, the compiler inserts synthetic casts at the points where you read generic values out. The main consequence is that generic type arguments are not reified, so you cannot ask at runtime what T was.

go deeper

for a junior

Can state that generics are a compile-time feature and the <String> is removed after compilation, so List<String> is just List at runtime.

for a middle

Knows the concrete rules: unbounded→Object, bounded→leftmost bound, raw type remains, compiler inserts casts — and why (backward compatibility, no JVM change).

for a senior

Explains the non-reified consequence chain (no new T()/T.class/instanceof List<String>/overload-by-argument) and contrasts with reified generics in C#.

for a principal

Discusses bridge methods, the Signature attribute retaining generic metadata, the migration-compatibility rationale, and trade-offs vs. reification/specialization (Valhalla).

## The problem generics solve Before Java 5 (2004), collections held `Object`. You wrote `List list = ...; String s = (String) list.get(0);` — manual casts everywhere, and a wrong cast blew up at runtime with `ClassCastException`. **Generics** let you write `List<String>` so the compiler verifies element types for you and inserts the casts automatically. ## What "type erasure" means Java had to add generics **without breaking the millions of existing class files** and **without changing the JVM**. The chosen technique is *type erasure*: the type arguments live only in the compiler's view of the program (the source and a little metadata), and are **erased** — removed — from the actual runtime types. Define the key terms: - **Type parameter**: the placeholder you declare, e.g. the `T` in `class Box<T>` or the `E` in `interface List<E>`. - **Type argument**: the concrete type you plug in, e.g. the `String` in `Box<String>`. - **Erasure**: the mapping the compiler applies to remove parameters from signatures. ## The erasure rules (the mechanism) 1. **Unbounded type parameter erases to `Object`.** In `class Box<T> { T value; }`, after erasure the field is of type `Object`, and `T get()` becomes `Object get()`. 2. **Bounded type parameter erases to its leftmost bound.** In `class Box<T extends Number> { T value; }`, erasure replaces `T` with `Number`. With multiple bounds `T extends Number & Comparable<T>`, it erases to the **first/leftmost** bound (`Number`); the leftmost is normally chosen to be a class for this reason. 3. **Parameterized types lose their arguments.** `List<String>` and `List<Integer>` both erase to the **raw type** `List`. `List<String>[]` erases to `List[]`. 4. **The compiler inserts casts.** Because `Box.get()` now returns `Object` at the bytecode level, every call site `String s = box.get();` gets a synthetic `(String)` cast injected by the compiler. You never wrote it, but it is in the bytecode, preserving type safety. 5. **Bridge methods** may be generated to make erasure work with polymorphism/overriding, but that is a deeper detail. ## The big consequence: generics are not reified *Reified* means "the type information exists at runtime." In Java generics are **non-reified** — erased. Therefore: - `new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass()` is `true` (both are `ArrayList.class`). - You **cannot** write `new T()`, `T.class`, `new T[10]`, or `obj instanceof List<String>` — the type isn't there at runtime. - You cannot overload two methods whose signatures differ only by type argument (`void f(List<String>)` and `void f(List<Integer>)`) — after erasure they are the same method. ## Why this is the trade-off Erasure buys **backward compatibility** (old code interoperates with generic code via raw types) and **zero runtime cost / no class bloat** (one `ArrayList` class serves all arguments, unlike C++ templates or C# reified generics which generate specialized code). The price is the lost runtime type information, which forces workarounds like passing `Class<T>` tokens (`Class.cast`), the `@SuppressWarnings("unchecked")` you sometimes need, and the inability to reflectively recover `T`.

  • If erasure removes the type, why don't you get a ClassCastException when reading from a List<String>?
    Because the compiler already proved the list only ever received Strings, and it inserts the matching cast at the read site. The cast can only fail if you defeated the compiler (e.g. via a raw type or unchecked cast).
  • Does erasure mean there is zero generic type info in the class file?
    Not quite — signatures of fields, methods, and supertypes retain generic info as metadata (the Signature attribute) for compilation against the class, but the executable types/instances are erased, so it isn't available for runtime decisions on an object.

saying these in an interview costs you the question

  • Saying List<String> and List<Integer> are different classes at runtime
  • Believing the type argument can be recovered at runtime via reflection on the object
  • Confusing erasure with C++ template instantiation (no per-type code is generated)
  • Thinking erasure causes runtime overhead — it's the opposite

context