Zum Hauptinhalt springen

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:

  1. What is type erasure?
  2. What does T become?
  3. What can I not do because of erasure?
  4. Why are arrays different?
  5. 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 TObject
  • T extends NumberNumber
  • 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

SurvivesDoes 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 declarationsinstanceof List<String>
Superclass signatures on a subclass that filled Tnew 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 T and 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.class is not List<Order>. Use a type token when the element type matters.

Next: Week 5 Day 4 — Wildcards and PECS