skip to content

What concrete operations does type erasure forbid or complicate, and what are the standard workarounds?

level: middleimportance: must knowfreq 70%

answer

  1. No new T[], no T.class, no instanceof List<String>
  2. Same-erasure overload clash
  3. No static field of type T; no catch(T e)
  4. Unchecked casts + heap pollution / @SafeVarargs
  5. Fix = pass Class<T> token or super type token

basics

~20 s

Because generic types are gone at runtime you cannot do new T[], T.class, or instanceof List<String>, and you cannot overload methods that differ only by type argument. Workarounds: pass a Class<T> token, use reflective array creation, or use a super type token.

solid answer

~50 s

Erasure forbids any operation that needs the type argument at runtime. You cannot create a generic array (`new T[n]`) or an array of a parameterized type, because the JVM checks array stores by runtime element type and the parameter is erased. You cannot use a type-parameter class literal (`T.class`) or test `instanceof List<String>` (only `List<?>`). Two methods whose signatures differ only by type argument have the *same erasure*, so they fail to compile. Static fields cannot be of a type-parameter type. Standard workarounds: pass a `Class<T>` token and use `clazz.cast(...)` or `Array.newInstance(clazz, n)` for runtime-typed arrays; use a *super type token* for full generic types; suppress and isolate unchecked casts behind a checked API. Also beware *heap pollution*: erasure lets a `List<String>` reference alias a list actually holding non-Strings via raw types, deferring the `ClassCastException` to read time.

code

java · 23 lines
java
import java.lang.reflect.Array;
import java.util.*;

final class TypedStack<T> {
    private final Class<T> type;          // the type, smuggled in as a value
    private T[] data;
    private int size;

    @SuppressWarnings("unchecked")
    TypedStack(Class<T> type, int cap) {
        this.type = type;
        this.data = (T[]) Array.newInstance(type, cap); // legal generic array via reflection
    }

    void push(T item) { data[size++] = type.cast(item); } // runtime-checked, unlike erased cast
    T pop()           { return data[--size]; }

    public static void main(String[] a) {
        TypedStack<String> s = new TypedStack<>(String.class, 4);
        s.push("x");
        System.out.println(s.pop()); // x
    }
}

go deeper

for a junior

Knows you cannot do new T[] or T.class and that List<String> and List<Integer> look the same at runtime.

for a middle

Lists the main restrictions (arrays, class literal, instanceof, same-erasure overloads) and applies the Class<T> token / Array.newInstance workaround.

for a senior

Adds heap pollution, @SafeVarargs, checked collections, narrow unchecked-cast suppression, and explains each restriction from the reified-arrays-vs-erased-generics tension.

for a principal

Designs erasure-safe generic APIs, reasons about where to localize unsafe casts, when to expose Class/Type tokens, and the soundness trade-offs of covariant arrays vs invariant generics.

## Why these restrictions exist Every restriction below comes from the same root cause: **at runtime the type argument is gone** (see the erasure topic). Any language feature that would need to *inspect or act on* that argument at runtime must therefore be forbidden or rerouted. ## 1. No `new T[]` and no `new List<String>[]` Arrays in Java are **covariant and reified**: an array remembers its element type at runtime and throws `ArrayStoreException` if you store the wrong type. Generics are **invariant and erased**. Mixing them would defeat the array store check, so the language bans creating arrays whose element type involves a type parameter or argument. ```java // T[] arr = new T[10]; // compile error: generic array creation // List<String>[] a = new List<String>[10]; // compile error ``` **Workaround:** create via reflection with a runtime `Class`, or create an `Object[]`/raw array and cast (with a localized `@SuppressWarnings("unchecked")`): ```java @SuppressWarnings("unchecked") T[] arr = (T[]) Array.newInstance(componentType, n); // componentType is a Class<T> you were given ``` ## 2. No `T.class` and limited `instanceof` `T.class` is illegal because there is no single runtime class for `T`. `instanceof List<String>` is illegal because the check could only ever verify `List`; only the *unbounded wildcard* form is allowed: ```java if (obj instanceof List<?>) { ... } // OK // if (obj instanceof List<String>) {} // compile error ``` **Workaround:** carry a `Class<T>` token and use `token.isInstance(obj)` / `token.cast(obj)`. ## 3. Same-erasure overload clash Two methods are distinct only if their *erased* signatures differ. These collide: ```java void m(List<String> s) {} void m(List<Integer> i) {} // compile error: same erasure m(List) ``` **Workaround:** rename, or differentiate by a non-generic parameter. ## 4. No type-parameter static members A type parameter belongs to an *instance* of the generic class, but `static` members are shared across all instantiations, so `static T field;` is illegal. ## 5. Unchecked casts and warnings Because the runtime cannot verify a cast to a parameterized type, casts like `(List<String>) obj` produce an **unchecked** warning — the JVM only checks `List`. Confine such casts to small, well-reviewed spots and annotate `@SuppressWarnings("unchecked")` narrowly. ## 6. Heap pollution Erasure makes it possible for a variable of a parameterized type to point to an object that violates that type, usually via raw types or unchecked casts: ```java List<String> ls = new ArrayList<>(); List raw = ls; // raw type raw.add(42); // no check at this point — heap polluted String s = ls.get(0); // ClassCastException only HERE ``` The failure is *deferred* to the read site because the `add` happened through an erased view. Varargs of generic types can also cause this — hence `@SafeVarargs`. ## 7. Catching a generic exception You cannot `catch (T e)` because exception matching is a runtime type check, which needs the (erased) concrete type. A generic class also cannot extend `Throwable`. ## The recurring fix: pass the type explicitly Nearly every workaround is the same move — *reintroduce the type as a runtime value*: a `Class<T>` token for single types (`enumMap`, `EnumSet.noneOf(Class)`, `Collections.checkedList`), or a **super type token** for full generic types. This is the engineering counterpart to the reflection topic: since the type isn't on the object, you must hand it in.

  • Why are arrays of parameterized types disallowed when generic collections are fine?
    Arrays are reified and covariant — they runtime-check element stores (ArrayStoreException). Generics are erased and invariant. A List<String>[] would have an erased element type at the store check, so the array's own safety guarantee couldn't be enforced; the language bans it to avoid silent type holes.
  • What does @SafeVarargs assert, and when is it appropriate?
    It suppresses the unchecked/heap-pollution warning on a varargs method whose parameter type is generic, asserting the method does not store anything unsafe into, nor leak, the implicitly-created generic array. Use it only on static/final/private methods you've verified never write a wrong-typed element into the varargs array or expose it.

saying these in an interview costs you the question

  • Thinking new T[] compiles, or that the warning is harmless
  • Using instanceof List<String> and expecting it to check the element type
  • Believing two overloads differing only by type argument are distinct
  • Treating an unchecked-cast warning as noise to blanket-suppress
  • Not knowing heap pollution defers ClassCastException to the read site

context