Question 13 · Section 20

What is type erasure

After compilation, the JVM doesn't know what type was specified in the generic.

Language versions: English Russian Ukrainian

🟢 Junior Level

Type erasure is a process where the Java compiler removes generic type information during compilation.

After compilation, the JVM doesn’t know what type was specified in the generic.

// Your code:
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();

// After compilation (type erasure):
List strings = new ArrayList();  // String -> Object
List integers = new ArrayList(); // Integer -> Object

// JVM sees just List, without type information!

Why it’s needed:

  • Compatibility with old code (before Java 5)
  • No overhead at runtime
  • Code works just as fast

🟡 Middle Level

How it works

The compiler replaces type parameters with their bounds (or Object if there is no bound):

// Source code
public class Box<T> {
    private T value;
    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

// After type erasure
public class Box {
    private Object value;  // T -> Object (no bound)
    public void set(Object value) { this.value = value; }
    public Object get() { return value; }
}

Bounded type erasure:

public class NumberBox<T extends Number> {
    private T value;
    public double doubleValue() { return value.doubleValue(); }
}

// After erasure: T -> Number (first bound)
public class NumberBox {
    private Number value;
    public double doubleValue() { return value.doubleValue(); }
}

Compiler adds casts:

Box<String> box = new Box<>();
box.set("Hello");
String s = box.get();

// After compilation:
Box box = new Box();
box.set("Hello");
String s = (String) box.get();  // implicit cast added

Restrictions due to type erasure

1. Cannot create instance of T:

public class Box<T> {
    // ❌ Cannot do
    public Box() {
        value = new T();  // compilation error
    }
}

2. Cannot instanceof T:

public void check(Object obj) {
    // ❌ Cannot do
    if (obj instanceof T) { }  // compilation error
}

3. Cannot create array of T:

public class Box<T> {
    // ❌ Cannot do
    private T[] array = new T[10];  // compilation error

    // ✅ Solution
    private T[] array = (T[]) new Object[10];
}

4. Cannot have static field with T:

public class Box<T> {
    // ❌ Cannot do — static context doesn't know about T
    private static T value;  // compilation error
}

🔴 Senior Level

Internal Implementation

JLS Specification (4.6):

Type erasure mapping:
1. Type parameter -> first bound (or Object)
2. Parameterized type -> erasure of raw type
3. Nested type -> recursively applied

Bridge methods:

public class Node<T extends Comparable<T>> {
    private T data;
    public void setData(T data) { this.data = data; }
    public T getData() { return data; }
}

public class DateNode extends Node<Date> {
    @Override
    public void setData(Date data) { super.setData(data); }
    @Override
    public Date getData() { return super.getData(); }
}

// After erasure:
class Node {
    private Comparable data;
    public void setData(Comparable data) { }
    public Comparable getData() { return data; }
}

class DateNode extends Node {
    public void setData(Date data) { super.setData(data); }
    public Date getData() { return super.getData(); }

    // Bridge method to preserve polymorphism:
    // Bridge method with erased signature that delegates to the specific one
    public void setData(Comparable data) { setData((Date) data); }
    public Comparable getData() { return getData(); }
    // In DateNode the compiler generates:
    // public Object getData() { return this.getData(); } // calls Date getData()
    // This is NOT recursion — it calls the overloaded method with a different return signature.
}

Reflection:

// Check via reflection
public void inspect() {
    List<String> list = new ArrayList<>();

    // Type erasure — runtime doesn't know about String
    Type genericType = list.getClass().getGenericSuperclass();
    System.out.println(genericType);  // java.util.AbstractList<E>

    // But you can get information from field/method
    Field field = MyClass.class.getDeclaredField("stringList");
    ParameterizedType pt = (ParameterizedType) field.getGenericType();
    System.out.println(pt.getActualTypeArguments()[0]);  // class java.lang.String
}

Architectural Trade-offs

Type erasure vs reified generics:

Type Erasure (Java) Reified (C#, Kotlin)
Binary compatibility Full information at runtime
Zero overhead Runtime overhead
Restrictions (new T, instanceof) Full functionality
Bridge methods No bridge methods

Edge Cases

1. Generic array creation exception:

public class GenericArray<T> {
    private T[] elements;

    public GenericArray(int size) {
        // Cannot new T[], but can via Class
        elements = (T[]) Array.newInstance(
            Object.class, size
        );
        // This is still an unchecked cast. Array.newInstance(Object.class) creates Object[],
        // not T[]. A type-safe approach requires Class<T>.
    }
}

2. Generic exception handling:

public class ExceptionHandler {
    // Type erasure — cannot catch by generic type
    public void handle(Exception e) {
        // ❌ Cannot do
        // if (e instanceof RuntimeException<String>) { }

        // ✅ Check via class
        if (e instanceof RuntimeException) { }
    }
}

3. Heap pollution:

// Heap pollution — when generic collection contains wrong type
List rawList = new ArrayList();
rawList.add("Hello");

List<String> stringList = rawList;  // unchecked warning
String s = stringList.get(0);  // OK

rawList.add(42);  // heap pollution — Integer appeared in a list of Strings
Integer i = (Integer) stringList.get(1);  // ClassCastException!

// @SafeVarargs helps
@SafeVarargs
public static <T> void safeMethod(List<T>... lists) { }

Performance

Type erasure overhead:
- Compile time: type checking
- Runtime: Zero overhead
- Memory: No extra data
- Cast: Implicit casts on access

Benchmark:
Operation         | With generics | Without generics
------------------|---------------|-----------------
List.get()        | 1 ns          | 1 ns (+ cast)
Map.put()         | 5 ns          | 5 ns
Type check        | compile time  | runtime (ClassCastException risk)

Production Experience

Workarounds for type erasure:

// 1. Pass Class<T> for creating instances
public class Factory<T> {
    private final Class<T> type;

    public Factory(Class<T> type) {
        this.type = type;
    }

    public T create() throws Exception {
        return type.getDeclaredConstructor().newInstance();
    }
}

// 2. Type token for generics
public class TypeReference<T> {
    private final Type type;

    protected TypeReference() {
        ParameterizedType pt = (ParameterizedType)
            getClass().getGenericSuperclass();
        this.type = pt.getActualTypeArguments()[0];
    }

    public Type getType() { return type; }
}

// Usage (Jackson style)
User user = mapper.readValue(json, new TypeReference<User>() {});

Best Practices

// ✅ Use Class<T> for creating instances
public <T> T create(Class<T> type) { return type.newInstance(); }

// ✅ Wildcards for flexibility
public void process(List<? extends Number> numbers) { }

// ✅ @SafeVarargs for generic varargs
@SafeVarargs
public static <T> void safeMethod(T... items) { }

// ❌ new T()
// ❌ instanceof T
// ❌ new T[]
// ❌ Static context with T

🎯 Interview Cheat Sheet

Must know:

  • Type erasure — compiler replaces T with Object (or first bound) at compilation
  • Why: binary compatibility with pre-Java 5 code, zero runtime overhead
  • Restrictions: cannot new T(), instanceof T, new T[], static field with T
  • Bridge methods — compiler creates them to preserve polymorphism after erasure
  • Bounded erasure: <T extends Number> — T is replaced with Number, not Object
  • Heap pollution — when generic collection contains elements of wrong type

Common follow-up questions:

  • Why can’t you new T()? — After erasure T -> Object, compiler doesn’t know which constructor to call
  • Can you determine type T at runtime? — Not directly, but can via Class or TypeReference
  • What is a bridge method? — Synthetic method with erased signature, delegating to the original
  • Type erasure vs reified generics (C#)? — Java: compile-time only, C#: full information at runtime

Red flags (DO NOT say):

  • ❌ “JVM checks generic types at runtime” — Type erasure, JVM doesn’t see generic types
  • ❌ “new T() works via reflection” — Forbidden at compiler level
  • ❌ “Type erasure adds overhead” — Zero overhead, no extra data
  • ❌ “Can check instanceof T” — Compilation error, type is erased

Related topics:

  • [[11. What are Generics in Java]]
  • [[12. What are the advantages of using Generics]]
  • [[14. Can you create an array of generic type]]
  • [[20. What happens when you try to create an instance of generic type via new T()]]
  • [[25. What are bridge methods and why are they needed]]