What is type erasure, and what consequences and restrictions does it create for generic code (e.g. instanceof, generic arrays, overloading)?
answer
- Erasure = type params removed after compile (replaced by bound/Object)
- Chosen for migration/backward compatibility
- List<String> == List<Integer> at runtime
- Bans: parameterized instanceof, new T[], overload-by-type-arg, T.class
- Workaround: Class<T> type tokens; heap pollution is the hazard
basics
~20 sType erasure means the compiler removes generic type information after checking it, so at runtime a List<String> is just a List. Because of this you can't do obj instanceof List<String>, can't create new T[], and can't have two methods that differ only by their generic type argument.
solid answer
~50 s**Type erasure** is how Java implements generics: type parameters are checked at compile time, then *erased* — replaced by their bounds (or Object) — so the bytecode contains no generic type info. `List<String>` and `List<Integer>` are the **same class** `List` at runtime. This was chosen for **migration compatibility** with pre-generics code. The consequences: you can't use a parameterized type in `instanceof` (`o instanceof List<String>` is illegal; only `List<?>` or raw `List`), you can't create a generic array (`new T[n]` and `new List<String>[n]` are forbidden because arrays need a reified runtime type), you can't overload on type arguments alone (`f(List<String>)` and `f(List<Integer>)` have the same erased signature and clash), a type parameter has no runtime `.class`, and you can't `catch` a generic exception type. Erasure also enables **heap pollution**. Workarounds include passing a `Class<T>` token (type tokens) when you need the runtime type.
code
java · 20 lines// 1) No parameterized instanceof
// if (o instanceof List<String>) {} // COMPILE ERROR
if (o instanceof List<?> list) { /* ok */ }
// 2) No generic array creation
// List<String>[] arr = new List<String>[10]; // COMPILE ERROR
// 3) No overload by type argument (same erasure)
// void f(List<String> s) {}
// void f(List<Integer> i) {} // COMPILE ERROR: name clash
// 4) Type token workaround when you need the runtime type
static <T> T create(Class<T> cls) throws Exception {
return cls.getDeclaredConstructor().newInstance(); // T.class is impossible
}
StringBuilder sb = create(StringBuilder.class);
// Proof that the two parameterizations share one runtime class:
boolean same = new ArrayList<String>().getClass()
== new ArrayList<Integer>().getClass(); // truego deeper
Know that generics are checked at compile time and removed (erased) afterward, so a List<String> is just a List at runtime.
Explain the main consequences — no parameterized instanceof, no new T[] — and that the two parameterizations are the same runtime class.
Enumerate the restrictions (overloading, T.class, static fields, generic exceptions), apply the Class<T> type-token workaround, and connect erasure to heap pollution and unchecked warnings.
Discuss the migration-compatibility rationale, contrast with reified generics, leverage retained signature metadata (super type tokens) in framework design, and weigh erasure's costs when designing generic APIs and serialization layers.
## What erasure is Java generics are a **compile-time** feature. The compiler uses type parameters to check your code, then **erases** them from the bytecode: each type parameter is replaced by its **leftmost bound**, or by `Object` if it's unbounded. `List<String>`, `List<Integer>`, and raw `List` all become the same runtime class, `java.util.List`. A field of type `T` becomes a field of type `Object` (or the bound). The compiler also inserts the **casts** that erasure removed, so your code still behaves type-correctly at runtime. ```java // What you write: class Box<T> { private T value; T get() { return value; } } // What the JVM effectively sees after erasure: class Box { private Object value; Object get() { return value; } } // ...and the compiler inserts (String) casts at call sites where T=String. ``` ## Why Java did this Generics were added in Java 5 (2004) to a language with an enormous installed base of non-generic collection code. **Erasure provided migration compatibility**: generic and legacy raw code interoperate, and old `.class` files keep working unchanged. The cost is that generic type information isn't available at runtime — unlike, say, C# generics, which are *reified* (kept at runtime). ## The restrictions erasure imposes **1. No parameterized `instanceof`.** Since `List<String>` and `List<Integer>` are indistinguishable at runtime, `o instanceof List<String>` is a **compile error**. You may only test the erased form: `o instanceof List` (raw) or `o instanceof List<?>` (unbounded wildcard). **2. No generic array creation.** `new T[10]` and `new List<String>[10]` are **forbidden**. Arrays are **reified** — they carry their element type at runtime and do an `ArrayStoreException` check on every write. A generic array's element type would be erased, so the check couldn't work, allowing silent heap pollution. The usual workarounds: create an `Object[]` and cast (suppressing the unchecked warning with proof), or use a `List<T>` instead of an array. **3. No overloading on type arguments alone.** `void f(List<String> x)` and `void f(List<Integer> x)` erase to the **same signature** `f(List)`, so they collide — the class won't compile ("name clash; have the same erasure"). **4. A type parameter has no runtime class.** You can't write `T.class`, `new T()`, or `T.staticMethod()`. If a generic method needs the actual runtime type, the idiom is to pass a **type token** — a `Class<T>` parameter, e.g. `<T> T create(Class<T> cls)` and call `cls.getDeclaredConstructor().newInstance()`. This is how APIs like `EnumSet.noneOf(Class)` and dependency-injection containers work. **5. No generic exception types.** You can't `catch (T e)` and a generic class can't extend `Throwable`, because exception matching is a runtime operation needing the actual type. **6. Static members can't use the class's type parameter.** `class Box<T> { static T x; }` is illegal — a static field is shared across all instantiations, but each could have a different `T`, so there's no single type. ## Heap pollution Because erasure lets generic and raw code mix, a variable of a parameterized type can end up referring to an object whose real contents violate that type — **heap pollution**. It's exactly what unchecked warnings warn about, and it's possible *because* the runtime no longer enforces the parameter. ## What still works at runtime Erasure isn't total. **Generic type information in signatures, fields, and supertypes is retained as metadata** and is readable via reflection (`getGenericType`, `getGenericSuperclass`, `ParameterizedType`). The "super type token" trick (an anonymous subclass like `new TypeReference<List<String>>(){}`) exploits this to recover a parameterized type at runtime — which is how libraries like Jackson and Spring capture generic types. ## The takeaway Erasure = generics are a compile-time fiction; the runtime sees raw types plus compiler-inserted casts, chosen for backward compatibility. Internalize the restrictions (no parameterized instanceof, no generic arrays, no overload-by-type-arg, no `T.class`) and the workaround (type tokens), and you'll understand most "why won't this generic code compile?" puzzles.
- If generics are erased, how do libraries like Jackson capture a List<String> at runtime?Via the *super type token* trick: an anonymous subclass such as `new TypeReference<List<String>>(){}` records `List<String>` as its generic superclass, and that metadata survives erasure (it's stored in the class file). Reflection (`getGenericSuperclass`, `ParameterizedType`) then recovers the full parameterized type.
- Why can't a static field use the enclosing class's type parameter?A static field is shared by every instantiation of the class — `Box<String>` and `Box<Integer>` share the same static storage. Each instantiation could supply a different `T`, so there's no single coherent type for the static field. Hence `static T field;` is illegal.
- How does C#'s approach differ from Java's?C# uses *reified* generics: type arguments are preserved at runtime, so you can do `typeof(T)`, `new T()`, parameterized `is List<int>`, and real generic arrays. Java chose erasure for backward compatibility with pre-generics bytecode, trading runtime type info for migration smoothness.
saying these in an interview costs you the question
- Claiming Java keeps generic type info at runtime like C# — it's erased.
- Saying List<String> and List<Integer> are different classes at runtime — they're the same.
- Thinking you can write `o instanceof List<String>` or `new T[n]` — both are compile errors.
- Believing you can overload methods that differ only in generic type arguments.
- Asserting erasure removes ALL generic info — signature/supertype metadata is retained and reflectable.