skip to content

What operations become impossible because of type erasure, and how do you work around them?

level: seniorimportance: must knowfreq 66%

answer

  1. Non-reified ⇒ can't use T at runtime
  2. Forbidden: new T(), T.class, new T[], instanceof List<String>
  3. No overload clash by erasure; no static T; no generic Throwable
  4. Fix: pass Class<T> type token
  5. Nested generics: super-type-token via getGenericSuperclass

basics

~20 s

Because the type is gone at runtime, you can't do new T(), T.class, new T[10], or check obj instanceof List<String>. You also can't overload methods that differ only by their generic argument. The common fix is to pass a Class<T> token so the type is available at runtime.

solid answer

~40 s

Erasure makes generic type arguments **non-reified** — absent at runtime — which forbids any operation that needs `T` at runtime. You cannot: instantiate a type variable (`new T()`), get its class literal (`T.class`), create a generic array (`new T[]` / `new List<String>[]`), use `instanceof` with a parameterized type (`x instanceof List<String>` — only `List<?>` is allowed), catch a generic exception type, or have a static field of type `T`. You also can't overload two methods whose signatures collapse to the same erasure (`f(List<String>)` vs `f(List<Integer>)`). The standard workaround is the **type token**: accept a `Class<T> clazz` parameter and use `clazz.newInstance()`/`clazz.getDeclaredConstructor().newInstance()`, `clazz.cast(x)`, or `Array.newInstance(clazz, n)`. For nested generics, a `TypeReference`/super-type-token captures the type via an anonymous subclass and reflection on its `getGenericSuperclass()`.

go deeper

for a junior

Knows you can't write new T() and usually must pass a Class to create instances generically.

for a middle

Lists the main forbidden operations (new T(), T.class, new T[], instanceof parameterized) and explains the type-token workaround.

for a senior

Explains each limitation from the non-reified root cause, knows the overload-clash and static-field rules, and implements both Class<T> tokens and super-type tokens.

for a principal

Distinguishes object-level erasure from declaration-level Signature metadata, weighs array covariance vs. erasure soundness, and chooses API shapes (token vs. reflection vs. Valhalla specialization) accordingly.

## The root cause **Reified** means the type exists at runtime; Java generics are **erased / non-reified**, so by the time code runs, `T` is gone (replaced by `Object` or its bound), and a `List<String>` is just a `List`. Every limitation below is a direct consequence: *you can't use information that isn't there.* ## The forbidden operations (and why) 1. **`new T()`** — to allocate, the JVM needs the concrete class; `T` erased to `Object`/bound, so the compiler can't know what to instantiate. *Compile error.* 2. **`T.class`** — a class literal is a runtime constant pointing at a specific `Class`; `T` has none. *Compile error.* 3. **`new T[n]` and `new List<String>[n]`** — arrays are **reified** (they check their element type at runtime via `ArrayStoreException`), but the generic element type is erased, so the runtime check would be unenforceable/unsound. Generic array creation is therefore disallowed (`new List<String>[10]` is an error; `new List<?>[10]` is allowed). 4. **`x instanceof List<String>`** — `instanceof` is a runtime check, but the argument is erased; only the **unbounded wildcard** `x instanceof List<?>` (or raw `List`) is permitted. 5. **`catch (MyException<T> e)`** — exception matching is runtime; a generic exception type can't be reified, so a generic class can't extend `Throwable`. 6. **`static T field;`** — a static member is shared across all parameterizations, but each would want a different `T`; since there is no single runtime `T`, static fields/contexts can't use the class's type parameter. 7. **Overload by erasure clash** — `void f(List<String>)` and `void f(List<Integer>)` both erase to `f(List)`; two methods with the same erasure can't coexist. *Compile error: name clash.* ## The workarounds ### Type token (`Class<T>`) Pass the class explicitly so the type is available at runtime: ``` <T> T make(Class<T> clazz) throws Exception { return clazz.getDeclaredConstructor().newInstance(); // replaces new T() } <T> T[] array(Class<T> clazz, int n) { @SuppressWarnings("unchecked") T[] a = (T[]) java.lang.reflect.Array.newInstance(clazz, n); // replaces new T[n] return a; } ``` `clazz.cast(obj)` gives a checked cast, and `Class.isInstance(obj)` replaces `instanceof T`. This is exactly how `EnumSet.noneOf(Class)`, `Collections.checkedList`, and Jackson/JPA APIs accept a class. ### Super-type token (`TypeReference`) A single `Class<T>` can't capture nested generics like `List<Map<String,Integer>>`. The trick: subclass an abstract generic carrier anonymously so the type argument is baked into the class's `genericSuperclass`, then read it via reflection: ``` abstract class TypeRef<T> { final java.lang.reflect.Type type = ((java.lang.reflect.ParameterizedType) getClass().getGenericSuperclass()) .getActualTypeArguments()[0]; } Type t = new TypeRef<List<String>>(){}.type; // List<String> ``` Gson's `TypeToken` and Jackson's `TypeReference` use exactly this. It works because **the Signature metadata of a class/method/field is retained** even though instance-level type arguments are erased — so reflection can recover generic info from *declarations*, just not from arbitrary *objects*. ## The boundary to remember You **cannot** recover `T` from an object (`list.getClass()` is just `ArrayList`), but you **can** recover declared generic types via reflection on classes/methods/fields, and you can carry `T` at runtime yourself with a `Class<T>` token. Erasure removes runtime *use* of the argument; the workarounds re-supply it.

  • Why is new List<String>[10] illegal but new List<?>[10] allowed?
    Arrays are reified and do a runtime element-type check, but List<String> is erased so the check can't be enforced soundly. The unbounded wildcard List<?> carries no argument to enforce, so it's permitted.
  • How does Gson deserialize into a List<MyType> despite erasure?
    Via a super-type token: you create new TypeToken<List<MyType>>(){}, an anonymous subclass whose generic superclass retains List<MyType> in its Signature metadata, which Gson reads by reflection to know the element type.

saying these in an interview costs you the question

  • Claiming you can write new T() directly
  • Saying instanceof List<String> compiles (only List<?> does)
  • Believing a single Class<T> can capture List<Map<...>>
  • Thinking reflection can recover the type argument from any object instance
  • Asserting a generic class can extend Throwable

context