Type Erasure
Type erasure is the mechanism by which the Java compiler removes all generic type information during compilation, replacing type parameters with their erasures — either Object or the first bound of a bounded type parameter — and inserting casts where necessary to preserve type safety at the call site. A List<String> and a List<Integer> become indistinguishable List objects at runtime; no generic type information survives into bytecode. Type erasure was chosen as the implementation strategy for generics in Java 5 to preserve binary compatibility with pre-generics bytecode: existing .class files compiled without generics could interoperate with new generic code without recompilation. This entry covers how erasure works for type parameters and wildcards, bridge methods and why the compiler generates them, the specific runtime consequences of erasure (heap pollution, unchecked warnings, instanceof limitations, reflection behavior), and patterns for working around erasure including the type token pattern and super type tokens.
How Erasure Works — Type Parameters, Bounds, and Bridge Methods
// ── Erasure of type parameters ───────────────────────────────────────
// Source:
class Box<T> {
private T value;
public T get() { return value; }
public void set(T value) { this.value = value; }
}
// After erasure (what the compiler actually emits):
class Box {
private Object value; // T → Object (unbounded)
public Object get() { return value; }
public void set(Object value) { this.value = value; }
}
// ── Erasure with bounded type parameters ─────────────────────────────
// Source:
class SortedPair<T extends Comparable<T>> {
T first, second;
public int compare() { return first.compareTo(second); }
}
// After erasure (T → Comparable, the first bound):
class SortedPair {
Comparable first, second;
public int compare() { return first.compareTo(second); }
// compareTo(second) works because erasure of Comparable<T> is Comparable
}
// ── Compiler-inserted casts — transparent in source, visible in bytecode
Box<String> box = new Box<>();
box.set("hello");
String s = box.get(); // looks like get() returns String
// Bytecode equivalent (what actually runs at the JVM level):
// Object temp = box.get(); // erased return type is Object
// String s = (String) temp; // compiler-inserted checkcast instruction
// The cast is safe because the compiler tracked type info during compilation.
// ── Overloading and erasure collision ─────────────────────────────────
class Processor {
// These two methods have IDENTICAL erasures: void handle(List)
// void handle(List<String> list) { } // COMPILE ERROR
// void handle(List<Integer> list) { } // COMPILE ERROR — "duplicate method"
// Fix: use different method names, or a different structural parameter
void handleStrings(List<String> list) { }
void handleIntegers(List<Integer> list) { }
}
// ── Bridge methods — visible via reflection ───────────────────────────
import java.lang.reflect.Method;
class StringComparable implements Comparable<String> {
@Override
public int compareTo(String o) { return this.toString().compareTo(o); }
// Compiler generates bridge:
// public int compareTo(Object o) { return compareTo((String) o); }
}
for (Method m : StringComparable.class.getDeclaredMethods()) {
System.out.println(m.isBridge() + " " + m.getName() + " " + m.getParameterTypes()[0]);
}
// false compareTo class java.lang.String (the one you wrote)
// true compareTo class java.lang.Object (bridge, compiler-generated)
// ── Generics vanish completely at runtime ─────────────────────────────
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(strings.getClass() == integers.getClass()); // true — both are ArrayList
System.out.println(strings.getClass().getName()); // java.util.ArrayList
// There is no List<String>.class — only List.class:
// Class<?> c = List<String>.class; // COMPILE ERROR — generic type has no class literalRuntime Consequences — Heap Pollution, instanceof, and Unchecked Warnings
// ── Heap pollution — how it happens ─────────────────────────────────
@SuppressWarnings("unchecked")
List<String> polluted = (List<String>) (List<?>) new ArrayList<>(List.of(1, 2, 3));
// No exception here — erasure means the JVM sees only ArrayList
// Exception thrown HERE, not where the bad data entered:
String first = polluted.get(0); // ClassCastException: Integer cannot be cast to String
// The cast was inserted by the compiler at the read site, not the write site.
// ── Varargs + generics = automatic heap pollution source ──────────────
// Safe-looking call site:
static void addToLists(List<String>... lists) { // COMPILE WARNING: varargs heap pollution
Object[] array = lists; // arrays are covariant — legal
array[0] = new ArrayList<Integer>(); // no ArrayStoreException — just List[]
String s = lists[0].get(0); // ClassCastException at runtime
}
// @SafeVarargs suppresses warnings when you have manually verified safety:
@SafeVarargs
static <T> List<T> concat(List<? extends T>... lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists) result.addAll(list);
return result; // safe: only reads from lists, never writes via the varargs array
}
// ── instanceof and reifiability ───────────────────────────────────────
Object obj = new ArrayList<String>();
// Legal instanceof forms:
System.out.println(obj instanceof List<?>); // true — unbounded wildcard is reifiable
System.out.println(obj instanceof ArrayList<?>); // true — reifiable
System.out.println(obj instanceof List); // true — raw type is reifiable
// Illegal instanceof forms (compile errors):
// obj instanceof List<String> — COMPILE ERROR, generic type is not reifiable
// obj instanceof T — COMPILE ERROR inside generic method, T erased
// ── Reifiable vs non-reifiable — practical decision table ────────────
// Reifiable (instanceof OK):
// int, long, boolean ... (primitives)
// String, Integer, Object (non-generic classes)
// List, Map, Optional (raw types)
// List<?>, Map<?,?> (unbounded wildcards)
// int[] (primitive arrays)
//
// Non-reifiable (instanceof illegal):
// List<String>, Map<String,Integer>, Optional<T> (generic instantiations)
// T, E, K, V (type parameters)
// ── Generic arrays — why they are banned ─────────────────────────────
// List<String>[] arr = new List<String>[10]; // COMPILE ERROR
// Why: arrays enforce element type at runtime (ArrayStoreException):
Object[] strings = new String[3];
strings[0] = 42; // ArrayStoreException — runtime knows element type is String
// But a List[] (the erasure of List<String>[]) does NOT know element type:
// Object[] listsArr = new List[3];
// listsArr[0] = new ArrayList<Integer>(); // no ArrayStoreException — poison inserted silently
// Workaround: use List<List<String>> or suppress with @SuppressWarnings("unchecked"):
@SuppressWarnings("unchecked")
List<String>[] safeIfYouKnowWhatYoureDoing = new List[10]; // raw type array, manual discipline
// ── Unchecked warning: suppress only with documented proof of safety ──
// BAD — silences without verifying:
@SuppressWarnings("unchecked")
Map<String, Integer> map = (Map<String, Integer>) getUntypedMap();
// GOOD — documents the invariant:
// The contract of getUntypedMap() guarantees keys are String, values are Integer.
// No external code can access the backing map, so no heap pollution is possible.
@SuppressWarnings("unchecked")
Map<String, Integer> map2 = (Map<String, Integer>) getUntypedMap();Working Around Erasure — Type Tokens, Super Type Tokens, and Reflection
// ── Type token pattern — Class<T> as explicit runtime type info ───────
class TypedCache<T> {
private final Class<T> type;
private final Map<String, Object> store = new HashMap<>();
public TypedCache(Class<T> type) { this.type = type; }
public void put(String key, T value) { store.put(key, value); }
public T get(String key) {
Object raw = store.get(key);
return type.cast(raw); // safe cast using runtime Class<T>
}
public boolean contains(String key) {
return type.isInstance(store.get(key)); // instanceof via Class
}
}
TypedCache<String> cache = new TypedCache<>(String.class);
cache.put("greeting", "hello");
String s = cache.get("greeting"); // no unchecked cast, type-safe
// Limitation: no Class<List<String>> — only Class<List> (raw):
// TypedCache<List<String>> broken = new TypedCache<>(List<String>.class); // COMPILE ERROR
// ── Super type token — capturing generic types at runtime ─────────────
abstract class TypeToken<T> {
private final Type type;
protected TypeToken() {
// getGenericSuperclass() returns ParameterizedType for TypeToken<List<String>>
ParameterizedType superclass = (ParameterizedType) getClass().getGenericSuperclass();
this.type = superclass.getActualTypeArguments()[0];
}
public Type getType() { return type; }
@Override public String toString() { return type.getTypeName(); }
}
// Usage: anonymous subclass captures the full generic type in bytecode:
TypeToken<List<String>> listOfString = new TypeToken<List<String>>() {};
TypeToken<Map<String,Integer>> mapType = new TypeToken<Map<String,Integer>>() {};
System.out.println(listOfString); // java.util.List<java.lang.String>
System.out.println(mapType); // java.util.Map<java.lang.String, java.lang.Integer>
// The type is now available as a ParameterizedType at runtime:
ParameterizedType pt = (ParameterizedType) listOfString.getType();
System.out.println(pt.getRawType()); // interface java.util.List
System.out.println(pt.getActualTypeArguments()[0]); // class java.lang.String
// ── Reflection and generic signatures — what IS preserved ────────────
import java.lang.reflect.*;
class Repository {
public List<String> findAll() { return List.of(); }
public <T extends Serializable> Map<String, T> findByType(Class<T> type) { return Map.of(); }
}
Method findAll = Repository.class.getMethod("findAll");
// getReturnType() gives the erased type:
System.out.println(findAll.getReturnType()); // interface java.util.List (erased)
// getGenericReturnType() gives the full declared type from bytecode signatures:
System.out.println(findAll.getGenericReturnType()); // java.util.List<java.lang.String>
ParameterizedType returnPT = (ParameterizedType) findAll.getGenericReturnType();
System.out.println(returnPT.getActualTypeArguments()[0]); // class java.lang.String
// ── What is and is not preserved after erasure ────────────────────────
// ERASED (not available at runtime on objects):
// - Type arguments on object instances: new ArrayList<String>() loses <String>
// - Type arguments in local variable declarations: List<String> x — erased
// - Type parameters inside method bodies
// PRESERVED (accessible via reflection):
// - Generic superclass/interfaces of a class: getGenericSuperclass(), getGenericInterfaces()
// - Generic return types/parameter types/field types: getGenericReturnType() etc.
// - Type parameter declarations on classes/methods: getTypeParameters()
// - Generic type info in anonymous subclasses that capture a supertype (super type token)
class StringList extends ArrayList<String> {} // <String> is preserved here
Type supertype = StringList.class.getGenericSuperclass();
System.out.println(supertype); // java.util.ArrayList<java.lang.String>