Type Erasure and Advanced Generics
Understand type erasure, heap pollution, reifiable types, and advanced generic patterns like type tokens.
Overview
Key Points
- •Type erasure replaces type parameters with their bounds (or Object if unbounded) in bytecode
- •List<String> and List<Integer> are the same Class object at runtime: List.class
- •You cannot do instanceof List<String>; only instanceof List<?> or instanceof List
- •Generic arrays are not directly creatable: new T[10] does not compile
- •Heap pollution: a List<String> variable holding a List<Integer> at runtime
- •@SuppressWarnings("unchecked") silences erasure-related compiler warnings
- •Type tokens (passing Class<T>) let you recover type information at runtime
- •Super type tokens using anonymous subclasses allow capturing parameterized types
- •Reifiable types are types whose complete type info is available at runtime (arrays, raw types, primitives)
Examples
What type erasure removes and what remains
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
// At runtime, both are just ArrayList
System.out.println(strings.getClass() == integers.getClass()); // true
System.out.println(strings.getClass().getName()); // java.util.ArrayList
// Cannot check parameterized type at runtime
Object obj = strings;
System.out.println(obj instanceof List<?>); // OK: wildcard
// System.out.println(obj instanceof List<String>); // compile error
// After erasure, this is actually allowed (unchecked cast)
@SuppressWarnings("unchecked")
List<Integer> heapPolluted = (List<Integer>) (Object) strings;
strings.add("hello");
// Reading the integer list would cause ClassCastException at the get() site
}
}Heap pollution is when a variable of a parameterized type refers to an object of a different parameterized type. The JVM cannot detect this until an actual cast occurs (at get() time, not at the add() time).
Type token pattern to recover runtime type
import java.util.*;
import java.lang.reflect.*;
public class TypedList<T> {
private final Class<T> type;
private final List<Object> list = new ArrayList<>();
TypedList(Class<T> type) { this.type = type; }
public void add(T element) { list.add(element); }
public T get(int index) {
return type.cast(list.get(index)); // safe cast with runtime check
}
public boolean isCompatible(Object obj) {
return type.isInstance(obj);
}
public static void main(String[] args) {
TypedList<String> sl = new TypedList<>(String.class);
sl.add("hello");
System.out.println(sl.get(0)); // hello
System.out.println(sl.isCompatible("world")); // true
System.out.println(sl.isCompatible(42)); // false
}
}Passing Class<T> as a constructor argument is the "type token" pattern. It gives the class access to runtime type information that erasure would otherwise remove. Jackson and Gson use this pattern for deserialization.
Generic method bridge and bounded wildcards in practice
import java.util.*;
public class Main {
// Lower bounded wildcard: add elements to any list of Number or supertype
static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
// Upper bounded wildcard: read from any list of Number or subtype
static double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) total += n.doubleValue();
return total;
}
public static void main(String[] args) {
List<Integer> ints = new ArrayList<>();
addNumbers(ints);
System.out.println("Sum: " + sum(ints)); // 6.0
List<Double> doubles = List.of(1.5, 2.5, 3.0);
System.out.println("Sum doubles: " + sum(doubles)); // 7.0
}
}PECS: Producer Extends, Consumer Super. Use ? extends T when reading (producing values for you). Use ? super T when writing (consuming values you provide). This is why Collections.copy(dest, src) uses List<? super T> dest and List<? extends T> src.
Common Mistakes
- Trying to create a generic array with new T[n]. This does not compile. Instead, cast from Object[]: (T[]) new Object[n], or use a List<T>.
- Expecting instanceof to work with parameterized types. instanceof List<String> is a compile error. Use instanceof List<?> to check for any List, then cast with an unchecked warning.
- Ignoring @SuppressWarnings("unchecked") warnings without understanding them. Suppressing the warning does not remove the heap pollution risk. Only suppress when you are certain the cast is actually safe.
- Not understanding PECS: passing a List<Integer> where List<Number> is expected. List<Integer> is NOT a subtype of List<Number> due to invariance. Use List<? extends Number> to accept both.
Note
When you need to capture a parameterized type at runtime (e.g., to deserialize JSON into List<User>), use a super type token: new TypeReference<List<User>>(){} creates an anonymous subclass whose generic supertype is captured in bytecode and readable via getGenericSuperclass(). Jackson uses exactly this pattern.
Exercises
Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.
Completed: 0 / 2 exercises
Knowledge Check
Question 1 of 4