Type Erasure

Understand why type arguments vanish at runtime, which code that rules out, and the Class<T>, Supplier<T> and type-token workarounds.

SyntaxList<String> a; List<Integer> b; // same runtime class: ArrayList <T> T read(String json, Class<T> type) <T> T create(Supplier<T> factory) new TypeRef<List<User>>() {} // captures the full type

Generics are checked by the compiler and then erased. In the compiled class file, a type parameter is replaced by its bound, or by Object when it has none, and the compiler inserts casts where values come out. At runtime a List<String> and a List<Integer> are both plain ArrayList objects of the same class. This kept code written before generics (Java 5) working with code written after.

What erasure rules out

  • new T() and new T[n]: the runtime does not know what T is.
  • obj instanceof List<String> when obj is an Object: the check cannot be made. Test instanceof List<?> and check the elements.
  • T.class and catching a type parameter as an exception type.
  • Overloads that differ only by type argument. void save(List<User> u) and void save(List<Order> o) have the same erasure and do not compile together; give them different names.
  • Primitive type arguments, since Object cannot hold an int.

Heap pollution

Raw types and unchecked casts can put the wrong type into a generic collection. Nothing fails at the bad write, because the list is just an ArrayList of Object. The ClassCastException comes later, at the cast the compiler inserted where the value is read, which may be in a different class or a different request. That is why unchecked warnings deserve attention: they mark the place where the compiler could not keep its promise.

The workarounds

  • Pass a Class<T> token. type.isInstance(o) and type.cast(o) do the check erasure removed. JSON and database libraries use this: readValue(json, User.class).
  • Pass a Supplier<T> instead of calling new T(), for example ArrayList::new or StringBuilder::new.
  • Capture the type in a subclass. Erasure removes type arguments from objects, but class and method declarations keep their generic signatures. An anonymous subclass of TypeRef<List<User>> records List<User> in its declaration, and reflection can read it back. Jackson's TypeReference and Spring's ParameterizedTypeReference use this trick to deserialise a List<User>, which List.class alone cannot describe.

Why it matters in a service

Most of the time erasure is invisible. It shows up when deserialising JSON into a generic type, when a framework needs to know the entity type of a repository, and when legacy code with raw types feeds your typed code. Knowing that the type is gone at runtime explains why those APIs ask you for a Class or a type reference.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Pass User.class or a type reference to a JSON library so a request body is deserialised into the right type.
  • Filter a mixed list of domain events down to one event type with a Class<T> token.
  • Give a generic pool or factory a Supplier<T> so it can create instances without new T().

More examples

A Class<T> token restores the check

item instanceof T would not compile, because T is erased. The Class object survives at runtime, so isInstance and cast do the check instead.

Example · java
import java.util.ArrayList;
import java.util.List;

record Created(String id) {}
record Deleted(String id) {}

<T> List<T> ofType(List<?> items, Class<T> type) {
    List<T> out = new ArrayList<>();
    for (Object item : items) {
        if (type.isInstance(item)) {
            out.add(type.cast(item));
        }
    }
    return out;
}

void main() {
    List<Object> events = List.of(new Created("a"), new Deleted("b"), new Created("c"));
    List<Created> created = ofType(events, Created.class);
    List<Deleted> deleted = ofType(events, Deleted.class);
    IO.println(created);
    IO.println(deleted);
}
// Output:
// [Created[id=a], Created[id=c]]
// [Deleted[id=b]]

A Supplier instead of new T()

The pool cannot construct a T itself, so the caller passes a constructor reference. The second borrow reuses the returned buffer instead of creating another.

Example · java
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.function.Supplier;

class Pool<T> {
    private final Supplier<T> factory;
    private final Deque<T> free = new ArrayDeque<>();
    private int created;

    Pool(Supplier<T> factory) { this.factory = factory; }

    T borrow() {
        if (free.isEmpty()) {
            created++;
            return factory.get();   // new T() would not compile
        }
        return free.pop();
    }

    void giveBack(T item) { free.push(item); }

    int created() { return created; }
}

void main() {
    Pool<StringBuilder> buffers = new Pool<>(StringBuilder::new);
    StringBuilder a = buffers.borrow();
    a.append("response body");
    a.setLength(0);
    buffers.giveBack(a);

    StringBuilder b = buffers.borrow();
    IO.println(a == b);
    IO.println("created: " + buffers.created());
}
// Output:
// true
// created: 1

Reading a type argument from a subclass

Each anonymous subclass declares its superclass with the full type argument, and that declaration survives erasure. List.class on its own only knows its parameter is called E, which is why JSON libraries ask for a type reference to read a List<User>.

Example · java
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;

abstract class TypeRef<T> {
    final Type type;

    protected TypeRef() {
        ParameterizedType superType = (ParameterizedType) getClass().getGenericSuperclass();
        type = superType.getActualTypeArguments()[0];
    }
}

void main() {
    Type listOfStrings = new TypeRef<List<String>>() {}.type;
    Type nested = new TypeRef<Map<String, List<Integer>>>() {}.type;
    IO.println(listOfStrings);
    IO.println(nested);

    IO.println(List.class.getTypeParameters()[0]);
}
// Output:
// java.util.List<java.lang.String>
// java.util.Map<java.lang.String, java.util.List<java.lang.Integer>>
// E

Discussion

  • Be the first to comment on this lesson.