AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

13 Generics / Lesson 5 of 6

Type erasure and its consequences

14 min · +10 XP · 2 exercises · 5 questions

Read the text14 min
Exercises0/2
Quiz5 q

Quick answer

What is type erasure in Java?

Type erasure is how Java implements generics: the compiler checks every generic use, then replaces each type parameter with Object or its first bound and inserts casts where values are read. At run time List<String> and List<Integer> are the same List class. Erasure kept older code and JVMs compatible, and it is why new T() and new T[n] are illegal.

Everything the compiler knows about List<String> it forgets before the class file is written. That decision — erasure — is why Java generics are backward compatible, and why a whole list of natural-looking things are impossible: new T(), T[], instanceof List<String>, overloading on type arguments, a static field of type T. This lesson explains what erasure does, walks through each limitation with the reason, and covers the two artefacts it leaves behind — bridge methods and heap pollution.

What erasure does

The compiler:

  1. Checks every generic use for type correctness.
  2. Replaces each type parameter by its erasure: Object for an unbounded T, the first bound for a bounded one (<T extends Number> → Number).
  3. Inserts casts where generic code returns a T to typed code: String s = list.get(0) becomes String s = (String) list.get(0).
  4. Generates bridge methods where erasure would break overriding (below).

After that, List<String>, List<Integer> and raw List are the same class at run time. A JVM from 2004 can run today's generic code.

new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass()      // true
List<String> a = …; a.getClass().getName()                                     // "java.util.ArrayList" — no <String>

Contrast C#, whose generics are reified: List<string> and List<int> are distinct run-time types. Java chose compatibility; the limitations below are the cost.

What you cannot do, and why

1. Instantiate a type parameter

T t = new T();               // error: unexpected type

T is Object at run time; there is no class to instantiate. Fix: pass a Supplier<T> (factory.get()) or a Class<T> token (clazz.getDeclaredConstructor().newInstance()).

2. Create a generic array

T[] arr = new T[10];                    // error: generic array creation
List<String>[] lists = new List<String>[3];   // error too

Arrays check their element type at run time (ArrayStoreException); a T[] would have no type to check against. Fix: (T[]) new Object[10] with a contained @SuppressWarnings("unchecked") (what ArrayList does), or Array.newInstance(clazz, 10) with a class token, or use a List<T>.

3. Test a type argument at run time

if (obj instanceof List<String>) …      // error: illegal generic type for instanceof
if (obj instanceof List<?>) …           // fine — the raw class is all that exists
List<String> l = (List<String>) obj;    // compiles with an UNCHECKED warning: the cast checks only "is it a List"

The JVM cannot see the String; the cast is a promise, not a check. If the promise is wrong, the ClassCastException appears later, at the first String s = l.get(0).

4. Use a type parameter in a static context

class Box<T> { static T shared; }       // error

One class serves every T; a static field would have to be all of them at once.

5. Overload on type arguments

void process(List<String> xs) { }
void process(List<Integer> xs) { }      // error: name clash — both erase to process(List)

Same erasure, same signature. Different method names, or one generic method.

6. Catch or throw a generic exception

class MyException<T> extends Exception { }     // error: a generic class may not extend Throwable

catch clauses are resolved by run-time class; a parameterised exception could not be distinguished.

7. Primitives as type arguments — List<int>: nothing to erase to. Wrappers.

Bridge methods

class Node<T> { T value; void set(T v) { value = v; } }
class IntNode extends Node<Integer> {
    @Override void set(Integer v) { … }         // overrides set(T)? After erasure the parent has set(Object)
}

set(Integer) does not have the same erased signature as set(Object), so the compiler generates a hidden bridge set(Object v) { set((Integer) v); } in IntNode so overriding works through the erased parent. You see bridges in stack traces and reflection (Method.isBridge()); you never write them. They are also why a ClassCastException can appear inside a bridge when heap pollution (next) delivers the wrong type.

Heap pollution

A variable of a parameterised type refers to an object that is not of that type:

List<String> strings = new ArrayList<>();
List raw = strings;
raw.add(42);                               // warning: unchecked call — the String list now holds an Integer
String s = strings.get(0);                 // ClassCastException here, with no cast visible in the source

Raw types, unchecked casts and generic varargs are the three ways to cause it. Every "unchecked" warning marks a place where erasure lets a lie through; treat them as errors, and confine the unavoidable ones ((T[]) new Object[n]) to the smallest scope with @SuppressWarnings("unchecked") and a comment.

Reifiable types

A type whose full information survives erasure is reifiable: primitives, non-generic classes, raw types, List<?> (unbounded wildcard), arrays of reifiable types. Only reifiable types may appear in instanceof, array creation and catch. Everything with a concrete type argument is non-reifiable.

Type tokens: carrying the type at run time

static <T> T fromJson(String json, Class<T> type) { … type.cast(parsed) … }
User u = fromJson(text, User.class);

static <T> T[] newArray(Class<T> type, int n) { return (T[]) Array.newInstance(type, n); }

A Class<T> parameter reintroduces the type at run time: it can create instances, arrays, and checked casts (type.cast(obj) throws a real ClassCastException immediately). Libraries that need List<User> (not just User) use a super type token — an anonymous subclass of a generic class whose superclass type argument is retained by reflection (new TypeReference<List<User>>() {} in Jackson) — the one place generic information does survive.

Reflection and generic signatures

Erasure removes type arguments from bytecode instructions, but declarations keep a Signature attribute: Field.getGenericType() returns List<String> for a field declared that way, and Class.getGenericSuperclass() is what super type tokens read. Declarations remember; values do not.

Interview angle

  • "What is type erasure?" The compiler replaces type parameters with their bounds and inserts casts; the JVM sees raw types.
  • "Why can't you write new T()?" At run time T is Object; there is no class to construct. Use a Supplier<T> or Class<T>.
  • "Why can't you overload f(List<String>) and f(List<Integer>)?" Both erase to f(List).
  • "What is a bridge method?" A compiler-generated method that lets an override with a specialised signature satisfy the erased parent signature.
  • "What is heap pollution?" A parameterised variable holding an object of the wrong type argument, via raw types or unchecked casts.

Key takeaways

  • Erasure: check at compile time, erase to Object/bound, insert casts. List<String> is List at run time.
  • Consequences: no new T(), no T[], no instanceof List<String>, no static T, no overloading on type arguments, no generic exceptions, no primitives.
  • Bridge methods keep overriding working; heap pollution is the lie unchecked warnings point at.
  • Class tokens (Class<T>) and super type tokens carry type information into run time when you need it.

Common questions

Why can't you create an instance of a type parameter with new T()?

At run time T has been erased to Object, so there is no class to instantiate. Pass the knowledge in instead: a Supplier<T> and call factory.get(), or a Class<T> token and call getDeclaredConstructor().newInstance().

Why can't you overload methods on generic type arguments?

Because both erase to the same signature. process(List<String>) and process(List<Integer>) both become process(List), and the compiler reports a name clash. Give the methods different names, or write one generic method.

What is a bridge method in Java?

A bridge method is a hidden method the compiler generates so an override with a specialised signature still overrides the erased parent. If IntNode extends Node<Integer> overrides set(Integer), the compiler adds a set(Object) that casts and calls it; Method.isBridge() reveals them.

What is heap pollution in Java?

Heap pollution is a variable of a parameterised type referring to an object that is not of that type, such as a List<String> holding an Integer after a raw-type add. Raw types, unchecked casts and generic varargs cause it, and it surfaces later as a ClassCastException where no cast is visible.

Why is instanceof List<String> illegal in Java?

The JVM cannot see the String: after erasure only the raw List class exists, so the test could not be carried out. instanceof List<?> is allowed, and a cast to List<String> compiles only with an unchecked warning, checking nothing beyond "is it a List".

Mark as readSign in to keep your place.

What Is Type Erasure in Java? Bridge Methods and Limits

Type erasure strips Java generic type arguments after compiling: a List of String is a plain List at run time. Why new T fails, bridge methods, heap pollution.

  • Course: Java study plan
  • Module: Generics
  • Kind: Lesson
  • Reading time: 14 min
  • Runtime: OpenJDK 18

What is type erasure in Java?

Type erasure is how Java implements generics: the compiler checks every generic use, then replaces each type parameter with Object or its first bound and inserts casts where values are read. At run time List<String> and List<Integer> are the same List class. Erasure kept older code and JVMs compatible, and it is why new T() and new T[n] are illegal.

Lesson

Everything the compiler knows about List<String> it forgets before the class file is written. That decision — erasure — is why Java generics are backward compatible, and why a whole list of natural-looking things are impossible: new T(), T[], instanceof List<String>, overloading on type arguments, a static field of type T. This lesson explains what erasure does, walks through each limitation with the reason, and covers the two artefacts it leaves behind — bridge methods and heap pollution.

What erasure does

The compiler:

  1. Checks every generic use for type correctness.
  2. Replaces each type parameter by its erasure: Object for an unbounded T, the first bound for a bounded one (<T extends Number> → Number).
  3. Inserts casts where generic code returns a T to typed code: String s = list.get(0) becomes String s = (String) list.get(0).
  4. Generates bridge methods where erasure would break overriding (below).

After that, List<String>, List<Integer> and raw List are the same class at run time. A JVM from 2004 can run today's generic code.

new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass()      // true
List<String> a = …; a.getClass().getName()                                     // "java.util.ArrayList" — no <String>

Contrast C#, whose generics are reified: List<string> and List<int> are distinct run-time types. Java chose compatibility; the limitations below are the cost.

What you cannot do, and why

1. Instantiate a type parameter

T t = new T();               // error: unexpected type

T is Object at run time; there is no class to instantiate. Fix: pass a Supplier<T> (factory.get()) or a Class<T> token (clazz.getDeclaredConstructor().newInstance()).

2. Create a generic array

T[] arr = new T[10];                    // error: generic array creation
List<String>[] lists = new List<String>[3];   // error too

Arrays check their element type at run time (ArrayStoreException); a T[] would have no type to check against. Fix: (T[]) new Object[10] with a contained @SuppressWarnings("unchecked") (what ArrayList does), or Array.newInstance(clazz, 10) with a class token, or use a List<T>.

3. Test a type argument at run time

if (obj instanceof List<String>) …      // error: illegal generic type for instanceof
if (obj instanceof List<?>) …           // fine — the raw class is all that exists
List<String> l = (List<String>) obj;    // compiles with an UNCHECKED warning: the cast checks only "is it a List"

The JVM cannot see the String; the cast is a promise, not a check. If the promise is wrong, the ClassCastException appears later, at the first String s = l.get(0).

4. Use a type parameter in a static context

class Box<T> { static T shared; }       // error

One class serves every T; a static field would have to be all of them at once.

5. Overload on type arguments

void process(List<String> xs) { }
void process(List<Integer> xs) { }      // error: name clash — both erase to process(List)

Same erasure, same signature. Different method names, or one generic method.

6. Catch or throw a generic exception

class MyException<T> extends Exception { }     // error: a generic class may not extend Throwable

catch clauses are resolved by run-time class; a parameterised exception could not be distinguished.

7. Primitives as type arguments — List<int>: nothing to erase to. Wrappers.

Bridge methods

class Node<T> { T value; void set(T v) { value = v; } }
class IntNode extends Node<Integer> {
    @Override void set(Integer v) { … }         // overrides set(T)? After erasure the parent has set(Object)
}

set(Integer) does not have the same erased signature as set(Object), so the compiler generates a hidden bridge set(Object v) { set((Integer) v); } in IntNode so overriding works through the erased parent. You see bridges in stack traces and reflection (Method.isBridge()); you never write them. They are also why a ClassCastException can appear inside a bridge when heap pollution (next) delivers the wrong type.

Heap pollution

A variable of a parameterised type refers to an object that is not of that type:

List<String> strings = new ArrayList<>();
List raw = strings;
raw.add(42);                               // warning: unchecked call — the String list now holds an Integer
String s = strings.get(0);                 // ClassCastException here, with no cast visible in the source

Raw types, unchecked casts and generic varargs are the three ways to cause it. Every "unchecked" warning marks a place where erasure lets a lie through; treat them as errors, and confine the unavoidable ones ((T[]) new Object[n]) to the smallest scope with @SuppressWarnings("unchecked") and a comment.

Reifiable types

A type whose full information survives erasure is reifiable: primitives, non-generic classes, raw types, List<?> (unbounded wildcard), arrays of reifiable types. Only reifiable types may appear in instanceof, array creation and catch. Everything with a concrete type argument is non-reifiable.

Type tokens: carrying the type at run time

static <T> T fromJson(String json, Class<T> type) { … type.cast(parsed) … }
User u = fromJson(text, User.class);

static <T> T[] newArray(Class<T> type, int n) { return (T[]) Array.newInstance(type, n); }

A Class<T> parameter reintroduces the type at run time: it can create instances, arrays, and checked casts (type.cast(obj) throws a real ClassCastException immediately). Libraries that need List<User> (not just User) use a super type token — an anonymous subclass of a generic class whose superclass type argument is retained by reflection (new TypeReference<List<User>>() {} in Jackson) — the one place generic information does survive.

Reflection and generic signatures

Erasure removes type arguments from bytecode instructions, but declarations keep a Signature attribute: Field.getGenericType() returns List<String> for a field declared that way, and Class.getGenericSuperclass() is what super type tokens read. Declarations remember; values do not.

Interview angle

  • "What is type erasure?" The compiler replaces type parameters with their bounds and inserts casts; the JVM sees raw types.
  • "Why can't you write new T()?" At run time T is Object; there is no class to construct. Use a Supplier<T> or Class<T>.
  • "Why can't you overload f(List<String>) and f(List<Integer>)?" Both erase to f(List).
  • "What is a bridge method?" A compiler-generated method that lets an override with a specialised signature satisfy the erased parent signature.
  • "What is heap pollution?" A parameterised variable holding an object of the wrong type argument, via raw types or unchecked casts.

Key takeaways

  • Erasure: check at compile time, erase to Object/bound, insert casts. List<String> is List at run time.
  • Consequences: no new T(), no T[], no instanceof List<String>, no static T, no overloading on type arguments, no generic exceptions, no primitives.
  • Bridge methods keep overriding working; heap pollution is the lie unchecked warnings point at.
  • Class tokens (Class<T>) and super type tokens carry type information into run time when you need it.

Common questions

Why can't you create an instance of a type parameter with new T()?

At run time T has been erased to Object, so there is no class to instantiate. Pass the knowledge in instead: a Supplier<T> and call factory.get(), or a Class<T> token and call getDeclaredConstructor().newInstance().

Why can't you overload methods on generic type arguments?

Because both erase to the same signature. process(List<String>) and process(List<Integer>) both become process(List), and the compiler reports a name clash. Give the methods different names, or write one generic method.

What is a bridge method in Java?

A bridge method is a hidden method the compiler generates so an override with a specialised signature still overrides the erased parent. If IntNode extends Node<Integer> overrides set(Integer), the compiler adds a set(Object) that casts and calls it; Method.isBridge() reveals them.

What is heap pollution in Java?

Heap pollution is a variable of a parameterised type referring to an object that is not of that type, such as a List<String> holding an Integer after a raw-type add. Raw types, unchecked casts and generic varargs cause it, and it surfaces later as a ClassCastException where no cast is visible.

Why is instanceof List<String> illegal in Java?

The JVM cannot see the String: after erasure only the raw List class exists, so the test could not be carried out. instanceof List<?> is allowed, and a cast to List<String> compiles only with an unchecked warning, checking nothing beyond "is it a List".

Exercises

Class tokens beat erasure

Write static <T> T[] filled(Class<T> type, int n, Supplier<T> factory) that creates a real T[] with java.lang.reflect.Array.newInstance and fills it from the supplier — the two things erasure forbids directly (new T[n], new T()). Then write static <T> int countOfType(Object[] items, Class<T> type) using type.isInstance.

Read an integer n. Build a String[] of n elements "s<index>" and an Integer[] of n squares with filled; print both with Arrays.toString, then strings=<count of String in a mixed Object[] made of both arrays> and array type=<the String[]'s getClass().getSimpleName()>.

Example: 3 →

[s0, s1, s2]
[0, 1, 4]
strings=3
array type=String[]

Erasure, demonstrated

Read n words. Show what erasure means at run time: print sameClass=<new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass()>, className=<that class's simple name>. Then pollute the heap: put the words in a List<String>, assign it to a raw List, add(42) through the raw reference, and read the elements back through the typed reference one by one — print each string, and when the ClassCastException fires print heap pollution at index <i>. Finally print size=<list size>: the integer is really in there.

Example: 2 then a b →

sameClass=true
className=ArrayList
a
b
heap pollution at index 2
size=3

In this module: Generics

← Wildcards and PECS · Generics in practice — idioms and error messages →