skip to content

What is type erasure in Java generics, and what happens to generic type information when code is compiled to bytecode?

level: juniorimportance: must knowfreq 78%

answer

  1. Compile-time check, then strip
  2. T → its bound (Object if unbounded)
  3. List<String> and List<Integer> share List.class
  4. Compiler inserts casts + bridge methods
  5. Done for backward / migration compatibility

basics

~20 s

Type erasure means the compiler removes generic type arguments after checking them, so at runtime a List<String> is just a List. The type parameters are replaced with their bounds (usually Object), and casts are inserted automatically.

solid answer

~40 s

Type erasure is the process by which the Java compiler uses generic type information only for compile-time type checking, then discards (erases) it so the generated bytecode contains no runtime generic type arguments. Each type parameter is replaced by its leftmost bound, or Object if it is unbounded; the compiler inserts synthetic casts where values are read out, and adds bridge methods to keep polymorphism working. As a result List<String> and List<Integer> share the same runtime class, List.class. Java did this for backward compatibility: pre-generics code and libraries keep working, and a single non-generic class file serves all instantiations (migration compatibility). The practical consequences are that you cannot do new T[], T.class, or instanceof List<String> at runtime, because that information is simply gone.

go deeper

for a junior

Can state that List<String> becomes plain List at runtime and that the compiler checks types then removes the brackets, inserting casts for you.

for a middle

Explains that T is replaced by its bound (Object if unbounded), why List.class is shared, the new T[]/T.class restrictions, and the backward-compatibility motivation.

for a senior

Adds bridge methods, leftmost-bound replacement, the same-erasure overload clash, and the key nuance that declared generic signatures survive in the Signature attribute even though runtime types are erased.

for a principal

Discusses the reified-vs-erased trade-off (migration compatibility vs runtime cost), Project Valhalla/specialization context, and designs APIs (super-type tokens, Class/Type parameters) that work around erasure deliberately.

## What problem generics solve Before Java 5, a `List` held `Object` references. You had to cast every element when reading it (`String s = (String) list.get(0);`), and nothing stopped you putting an `Integer` into a list meant for `String`s — the mistake only surfaced as a `ClassCastException` at runtime. **Generics** (Java 5, 2004) let you write `List<String>` so the *compiler* checks that only `String`s go in and automatically casts on the way out. This is purely a **compile-time** guarantee. ## What 'type erasure' means **Erasure** is the compiler step that, after it has finished type-checking your generic code, *removes* the generic type arguments and produces bytecode that looks almost exactly like the old pre-generics code. Concretely the compiler: 1. **Replaces each type parameter with its bound.** An unbounded `T` becomes `Object`. A bounded `T extends Number` becomes `Number` (the *leftmost* bound if there are several, e.g. `T extends Number & Comparable` → `Number`). 2. **Inserts casts** wherever a generic value is read, so the program still behaves type-safely. 3. **Generates bridge methods** — synthetic methods that preserve correct overriding/polymorphism after the type parameters are gone (e.g. so `compareTo(Object)` still routes to your `compareTo(MyType)`). So `List<String>` and `List<Integer>` both compile down to plain `List`, and there is exactly **one** runtime class: `List.class`. The angle-bracket information is not stored in the *type* of the object. ### Worked example Source: ```java List<String> list = new ArrayList<>(); list.add("hi"); String s = list.get(0); ``` After erasure the bytecode is equivalent to: ```java List list = new ArrayList(); list.add("hi"); String s = (String) list.get(0); // compiler-inserted cast ``` ## Why Java chose erasure The alternative — **reified** generics, where `List<String>` is a genuinely distinct runtime type (as in C#) — was rejected mainly for **migration/backward compatibility**. In 2004 there were billions of lines of existing pre-generics `List` code and compiled libraries. Erasure means a generified `ArrayList` produces a class file that old code can still use and that interoperates with raw types, and the JVM did not need new bytecode for parameterised types. The cost is paid at runtime: the generic information is *largely* gone. ## What erasure makes impossible Because the type argument is not part of the object's runtime type, you cannot, at runtime: - create an array of a type parameter: `new T[10]` (compile error); - get a class literal of a parameter: `T.class` (compile error); - test `obj instanceof List<String>` (only `instanceof List<?>` is allowed); - overload two methods that differ only by type argument (`m(List<String>)` and `m(List<Integer>)` have the **same erasure** → compile error). ## The important nuance (links to the rest of this topic) Erasure removes generics from the *object's runtime type*, but the compiler does **not** throw the information away entirely. For type *uses that appear in declarations* — a class's superclass, a field's type, a method's parameter and return types — the compiler records the full generic form in a class-file **`Signature` attribute**. The **reflection** API can read that attribute (`getGenericSuperclass`, `getGenericType`, `getGenericParameterTypes`) to recover the declared type arguments at runtime. So the right mental model is: *the runtime type is erased, but declared generic signatures are retained as metadata.*

  • If everything is erased, how can a library like Jackson or Gson sometimes deserialize into a List<MyType>?
    Because the generic type argument of a declared field/parameter/superclass is kept in the class-file Signature attribute. Libraries capture it via reflection (getGenericType) or via a TypeReference/super-type-token subclass whose getGenericSuperclass exposes the argument — not from the runtime object.
  • What is a bridge method and why does erasure require one?
    A synthetic method the compiler adds so overriding still works after erasure. E.g. when you implement Comparable<MyType>.compareTo(MyType), the interface's erased signature is compareTo(Object); the bridge method compareTo(Object) casts and delegates to compareTo(MyType) so dynamic dispatch finds your method.

saying these in an interview costs you the question

  • Saying List<String>.class exists or that List<String> is a distinct runtime type
  • Claiming Java generics are reified like C# generics
  • Believing you can do new T[] or T.class at runtime
  • Confusing erasure with autoboxing or with type inference
  • Thinking ALL generic info is gone (declared signatures are retained in the Signature attribute)

context