Dataslope logoDataslope

Type Erasure

What happens to a generic type at runtime, why arrays and generics don't mix, and the small set of rules that follows from erasure

Generics are a compile-time feature in Java. At runtime, the JVM knows almost nothing about them. This was a deliberate choice — it let Java 5 add generics without breaking any class file that predated them — but it has a small bag of consequences that every working Java programmer eventually trips on.

This page lays them out so you don't have to learn each one by crash.

What erasure actually does

When javac compiles a generic class, it erases the type parameter to its erasure (the most specific bound, usually Object):

// You write:
public class Box<T> {
    private T value;
    public T  get()       { return value; }
    public void put(T v)  { value = v; }
}

// The JVM sees, essentially:
public class Box {
    private Object value;
    public Object get()        { return value; }
    public void   put(Object v){ value = v; }
}

At the use sites, the compiler inserts casts to recover the declared type:

Box<String> b = new Box<>();
b.put("hi");
String s = b.get();        // compiler inserts: (String) b.get();

That is the whole trick. Generics give you compile-time guarantees; at runtime, they look like the old Object-based code, with the casts written for you.

Consequence 1: you cannot ask for the type parameter at runtime

There is no way to ask, at runtime, "what type parameter is this collection?" Because the answer literally is not stored anywhere.

Code Block
Java 8 (Update 492)

That is why if (x instanceof List<String>) is illegal. You can write if (x instanceof List<?>) (the wildcard says "I don't claim to know the parameter"), but not the specific form.

Consequence 2: you cannot create an array of a generic type

The JVM tracks element types for arrays at runtime — that is what makes new String[10] refuse a Date. Because generics are erased, the compiler cannot generate a meaningful element-type check for new T[10]. So it forbids it.

Code Block
Java 8 (Update 492)

In practice you almost never want a generic array — you want a List<T> or Map<K,V>. That's the JCF's whole point.

Consequence 3: you cannot overload by type parameter

Because both forms erase to the same bytecode signature, the following is forbidden:

// Not allowed — both erase to printer(List)
public void printer(List<String>  xs) { ... }
public void printer(List<Integer> xs) { ... }

If you need different behavior per type, give the methods different names, or take a Class<T> token, or use overloading by something the compiler can tell apart.

Consequence 4: static fields do not "see" the type parameter

There is only one Class object per generic class — not one per parameterization. So static state is shared across all parameterizations.

Code Block
Java 8 (Update 492)

Usually you do not want static state in a generic class anyway. When you find yourself reaching for it, that is a hint that the state belongs outside the generic class.

Consequence 5: heap pollution and the @SafeVarargs annotation

Varargs of a generic type (List<String>... lists) are arrays under the hood, which we just said doesn't mix with generics. The compiler warns about heap pollution: that the array's runtime element type is the erasure, not the generic type, so an unrelated value could sneak in.

If your method genuinely does not store anything bad into the array, you can suppress the warning with @SafeVarargs:

Code Block
Java 8 (Update 492)

This is exactly how the JDK declares List.of(E... e).

What erasure preserves

It is easy to think erasure throws everything away. Worth noting what it keeps:

  • Class metadata is intact. The bytecode signature on a class or method still records the generic information (in a separate attribute), which tools like the IDE and reflection's ParameterizedType can read.
  • Bridge methods. When a generic class's method gets specialized, javac generates extra "bridge" methods to keep polymorphism working. You can see them with javap -v.
  • Compile-time safety. This is the big one. The whole point of generics — the cast that never fails — survives erasure intact.

When erasure bites you in real life

Three patterns where erasure most often shows up in working code:

  1. You want to ask "is this a List of X?" You can't — only instanceof List<?>. If you need the element type, you have to pass a Class<X> token.
  2. You want to create a T[] inside a generic class. You can't — return List<T>, or take a Class<T> to do it reflectively (Array.newInstance).
  3. You want different static fields per parameterization. You can't — and probably don't want to.

Passing a Class<T> token: the standard workaround

When you genuinely need the type at runtime — for reading from JSON, constructing arrays, looking up in a registry — accept a Class<T> parameter:

Code Block
Java 8 (Update 492)

This is the trick the JDK uses in Collections.checkedList(List, Class) and many reflective APIs.

Test your understanding

QuestionSelect one

What does new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass() return at runtime?

false — they are different parameterized types

true — both share the same erasure (ArrayList)

A compile error

null

QuestionSelect one

Why is new T[10] illegal inside a generic class?

Arrays don't allow size 10

Arrays carry their element type at runtime, but T is erased — the JVM can't enforce the element type, so the language forbids the construction

It would always throw ClassCastException

The JVM only supports primitive arrays

QuestionSelect one

If you absolutely need to know the element type of a generic structure at runtime, what's the standard workaround?

Read a private field with reflection

Use instanceof List<X> checks

Accept a Class<T> token as a constructor or method parameter and use it for isInstance, cast, or array creation

Switch to a different language

On this page