Week 5 Day 3 — Type Erasure
Goal
Today I want a precise picture of what the JVM still knows after the compiler erases type parameters.
Main questions:
- What is type erasure?
- What does
Tbecome? - What can I not do because of erasure?
- Why are arrays different?
- Why does Spring need
ParameterizedTypeReference?
1. Erasure is the implementation
Generics are a compile-time tool. After the compiler checks the contract, it erases type parameters:
- unbounded
T→Object T extends Number→Number- it inserts the casts I no longer write
At runtime:
List<String> names = new ArrayList<>();
List<Integer> nums = new ArrayList<>();
names.getClass() == nums.getClass(); // true: both ArrayList
List<String> and List<Integer> are the same class. There is no String on the Class object of the list.
Memory sentence:
Generics disappear at runtime. The compiler already proved the casts.
2. What survives
| Survives | Does not survive |
|---|---|
The raw class (List, ArrayList) | The argument (String in List<String>) |
Bounds as the erased type (Number) | Overloads that differ only by T |
Reflection on fields: getGenericType() on declarations | instanceof List<String> |
Superclass signatures on a subclass that filled T | new T(), T[] |
Reflection twist: a field List<Order> orders has a ParameterizedType in the class file of my class. That is metadata on the declaration, not on a given list instance. orders.getClass() is still ArrayList.
A trick Spring uses: an anonymous subclass that fills T:
new ParameterizedTypeReference<List<Order>>() {}
The superclass is ParameterizedTypeReference<List<Order>>. Reflection can read that superclass argument from the anonymous class. The list instance itself still does not know.
3. Things erasure forbids
T value = new T(); // no
T[] array = new T[10]; // no
if (list instanceof List<String>) { } // no — illegal
if (list instanceof List<?>) { } // yes — unbounded wildcard
void handle(List<String> names) { }
void handle(List<Integer> nums) { } // no — both erase to handle(List)
I can use list instanceof List (raw) or List<?>.
Heap pollution: putting the wrong type into a generic container via raw types or unchecked casts. The ClassCastException happens later, on a generated cast.
Bridge methods: when I override a generic method with a more specific type, the compiler emits a synthetic method with the erased signature that casts and delegates. That is how compareTo(Object) still exists for Comparable<String>. I rarely write bridges; I should know they exist if I look at javap.
4. Arrays are reified and covariant
Arrays keep their component type at runtime (new String[2] is a String[]). They are also covariant: String[] is a Object[].
Object[] objects = new String[1];
objects[0] = 42; // compiles; ArrayStoreException at runtime
Generics are invariant and erased: List<String> is not a List<Object>, and the JVM cannot throw an equivalent of ArrayStoreException on list.add because it does not know String.
That is why I prefer List<Order> over Order[] in APIs, and why new T[] is illegal.
Memory sentence:
Arrays know their component type and are covariant. Lists forget
Tand are invariant.
5. Spring connection
HTTP clients need the element type of a list. Erasure deleted it:
// wrong mental model: List.class does not mean List<Order>
List<Order> body = rest.getForObject(url, List.class);
// right: a type token that survives as superclass metadata
List<Order> body = rest.exchange(
url, HttpMethod.GET, null,
new ParameterizedTypeReference<List<Order>>() {}
).getBody();
RestClient / RestTemplate / Jackson need that token to build List<Order>, not List<LinkedHashMap>.
JPA Class<T> on JpaRepository<Order, Long> is a class literal Order.class the infrastructure stores. That is a reified Class, not a surviving T on a list.
6. Common traps
Trap 1: if (x instanceof List<String>).
Trap 2: Expecting getClass().getTypeName() on an ArrayList to print ArrayList<Order>.
Trap 3: getForObject(url, List.class) and then wondering why elements are maps.
Trap 4: Overloading two methods that differ only by a type argument.
Trap 5: Object[] as a generic substitute. ArrayStoreException vs silent heap pollution — two different bugs. Prefer lists.
Practice Questions and Answers
Question 1
What is type erasure?
Answer:
The compiler checks generics, then deletes type parameters from the bytecode, replacing T with Object or the bound and inserting casts. At runtime List<String> and List<Integer> are both List.
Question 2
What survives at runtime?
Answer:
The raw class, the bound as the erased type, and generic signatures on declarations (fields, superclasses of a named/anonymous subclass). A live ArrayList instance does not carry Order.
Question 3
Why is instanceof List<String> illegal?
Answer:
instanceof is a runtime check. The parameter is already gone. I can write instanceof List<?> or instanceof List.
Question 4
Why does Spring’s ParameterizedTypeReference exist?
Answer:
Because List.class has no element type. An anonymous subclass records List<Order> on its superclass in the class file. Jackson and RestTemplate read that and deserialize a list of orders.
Question 5
Why prefer List<T> over T[] in APIs?
Answer:
Arrays are covariant and reified, so stores can fail at runtime (ArrayStoreException). I also cannot create T[] cleanly. Lists are invariant at compile time and are the collection contract I actually want. Erasure still applies, so I keep the compile-time parameter honest and avoid raw types.
Memory sentences
Generics disappear at runtime; the compiler already proved the casts.
Arrays are reified and covariant. Lists are erased and invariant.
List.classis notList<Order>. Use a type token when the element type matters.