All topics
library
advanced

Type Erasure

Understand how generics are implemented via erasure and the limitations this creates.

The compiler checks your generics, then deletes them. The bytecode has no idea T ever existed.

Erasure = writing a letter with invisible ink (generics). The recipient (JVM) can't see the invisible parts — they only see the regular ink (Object, casts).

Key Concepts

1
    // what you write
    class Box<T> { T value; T get() { return value; } }
    List<String> names = new ArrayList<>();
    String s = names.get(0);
2
    // what the bytecode holds
    class Box { Object value; Object get() { return value; } }
    List names = new ArrayList();
    String s = (String) names.get(0);     // cast inserted for you
3
An unbounded T becomes Object. A bounded one becomes its bound:
TObject
4
    <T>                     ->  Object
    <T extends Number>      ->  Number
    <T extends Comparable>  ->  Comparable
5
Why it was done this way: backward compatibility. Java 5 generics had to run on bytecode compiled before generics existed, and old code passing a plain List had to keep working. Erasure made List<String> and List the same class at runtime, so both directions compile.
ListList<String>
6
What it costs you.
    new T()                          the type is unknown at runtime
    new T[10]
    obj instanceof T
    T.class
    static T field                   statics are shared across all T
    f(List<String>) and f(List<Integer>)    same erasure, cannot overload
7
And this is the surprising consequence:
    List<String> a = new ArrayList<>();
    List<Integer> b = new ArrayList<>();
    a.getClass() == b.getClass();    // true -- both are ArrayList
8
So nothing at runtime can tell you what a collection was declared to hold.
9
The workaround is a type token — pass the class explicitly:
10
    <T> T parse(String json, Class<T> type) { ... }
    parse(body, Order.class);
11
Or capture it from a subclass. Generic information on a class declaration is kept in the class file, which is why new TypeReference<List<Order>>() {} works in Jackson: the anonymous subclass records the type even though a variable could not.
new TypeReference<List<Order>>() {}
12
What the interviewer is probing.1. "What is type erasure and why did Java choose it?" Probing: whether you know the trade-off, not just the term. Stalls: "Generics are removed at runtime." Moves up: it was for backward compatibility: generic code had to interoperate with pre-generic bytecode, so List<String> and List are the same class at runtime.
13
2. "List<String> and List<Integer> — same class at runtime?" Probing: the consequence of erasure. Stalls: "No, they differ." Moves up: yes, getClass() returns the same ArrayList, which is why you cannot overload on them or test them with instanceof.
14
3. "How does Jackson read the type for new TypeReference<List<Order>>() {}?" Probing: the exception to erasure. Stalls: "It guesses." Moves up: generic information on a class declaration is kept in the class file, so the anonymous subclass records the type a variable could not.
15
4. "How do you work around not being able to write T.class?" Probing: the type token idiom. Stalls: "You cannot." Moves up: pass Class<T> as a parameter and use it for the cast or instantiation, which is what parse(json, Order.class) does.