Type Erasure in Java: What Happens at Runtime

Illustration for Type Erasure in Java: What Happens at Runtime
By Last updated:

nerics in Java provide type safety and reusability at compile time. However, at runtime, Java uses a mechanism called type erasure, which removes type parameter information to maintain backward compatibility with legacy code written before Java 5.

This means that while your code may specify List<String> or List<Integer>, at runtime, both become simply List. This has implications for reflection, performance, and certain restrictions in generics (like why you can’t create new T()).

Think of type erasure as a blueprint being shredded after the house is built: the structure exists, but the exact details of the design (generic types) are gone.

In this tutorial, we’ll explore how type erasure works, why it exists, and how it affects your Java code.


Core Definition and Purpose of Java Generics

Generics allow developers to:

  1. Catch errors at compile time – Avoid ClassCastException.
  2. Write reusable APIs – Use one class/method for multiple data types.
  3. Maintain legacy compatibility – Achieved through type erasure.

Introduction to Type Parameters: <T>, <K, V>, <E>

  • <T> – General type.
  • <E> – Element (used in collections).
  • <K, V> – Key-Value pair (maps).

Example:

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

At runtime: Box<T>Box.


Generic Classes and Type Erasure

List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();

System.out.println(strings.getClass() == integers.getClass()); // true

Both lists are just ArrayList at runtime.


Generic Methods and Erasure

public static <T> void print(T item) {
    System.out.println(item);
}

After erasure, becomes:

public static void print(Object item) {
    System.out.println(item);
}

Bounded Type Parameters and Erasure

public <T extends Number> void display(T number) {
    System.out.println(number.doubleValue());
}

After erasure:

public void display(Number number) {
    System.out.println(number.doubleValue());
}

Wildcards and Type Erasure

List<? extends Number> list = new ArrayList<Integer>();

At runtime: List. Compiler enforces safety checks.


Multiple Type Parameters and Nested Generics

Map<String, List<Integer>> map = new HashMap<>();

At runtime: Map. No nested type info remains.


Raw Types vs Parameterized Types

List rawList = new ArrayList(); // Unsafe
List<String> safeList = new ArrayList<>(); // Type-safe at compile time

At runtime, both are simply ArrayList.


Type Inference and Diamond Operator

Map<String, Integer> map = new HashMap<>();

Compiler infers types. At runtime, it’s just HashMap.


Type Erasure in Java: Compile-Time vs Runtime

  • Compile-Time: Generic type checks enforced.
  • Runtime: Type parameters erased, replaced with bounds or Object.

Reifiable vs Non-Reifiable Types

  • Reifiable Types: Exist fully at runtime (int[], List<?>).
  • Non-Reifiable Types: Lose type info (List<String>, Map<K, V>).

Example:

if (obj instanceof List<String>) { } // Compile error
if (obj instanceof List<?>) { } // Allowed

Recursive Type Bounds with Erasure

public static <T extends Comparable<T>> T max(List<T> list) { ... }

After erasure:

public static Comparable max(List list) { ... }

PECS Principle and Erasure

Wildcards (extends/super) still apply at compile time. At runtime, erasure treats both as List.


Designing Fluent APIs under Erasure

Erasure prevents new T(). Instead, APIs use factories or reflection.

public static <T> T create(Class<T> clazz) throws Exception {
    return clazz.getDeclaredConstructor().newInstance();
}

Generics with Enums and Annotations

Erasure ensures enums and annotations can interact with generics without runtime type issues.


Generics with Exceptions: Why new T() is Not Allowed

Since type information is erased, T has no runtime type to instantiate.


Generics and Reflection with Type Erasure

Field field = MyClass.class.getDeclaredField("list");
System.out.println(field.getGenericType()); // java.util.List<java.lang.String>

Reflection preserves some generic metadata but not full type enforcement.


Case Studies

Type-Safe Cache

class Cache<K, V> {
    private Map<K, V> store = new HashMap<>();
    public V get(K key) { return store.get(key); }
}

Runtime: Map. Compile-time enforces type safety.

Flexible Repository Pattern

interface Repository<T, ID> {
    T findById(ID id);
}

Erased to Repository.

Event Handling System

interface Listener<E> {
    void onEvent(E event);
}

Erased to Listener.


Best Practices with Type Erasure

  • Don’t rely on generic types at runtime.
  • Use @SuppressWarnings("unchecked") sparingly.
  • Use reflection carefully for generics.
  • Prefer Class<T> tokens for creating objects.

Common Anti-Patterns

  • Checking instanceof List<String>.
  • Creating arrays of generic types (new T[]).
  • Deeply nested generics.

Performance Considerations

Generics add zero runtime overhead. Type checks happen at compile time, erased at runtime.


📌 What's New in Java for Generics?

  • Java 5: Generics introduced with erasure.
  • Java 7: Diamond operator improved inference.
  • Java 8: Streams and lambdas enhanced generics usage.
  • Java 10: var integrates with generics.
  • Java 17+: Sealed classes work with erased generics.
  • Java 21: Virtual threads leverage generics in concurrent APIs.

Conclusion and Key Takeaways

Type erasure ensures generics remain backward-compatible while enforcing compile-time type safety. At runtime, all parameterized types are erased, which:

  • Prevents certain features (new T(), instanceof List<String>).
  • Ensures no runtime cost for generics.
  • Encourages designing APIs with reflection or Class<T> tokens where needed.

FAQ on Type Erasure

Q1: Why does Java use type erasure?
To maintain backward compatibility with pre-Java 5 code.

Q2: What happens to List<String> at runtime?
It becomes just List.

Q3: Why can’t I do instanceof List<String>?
Type information is erased at runtime.

Q4: What’s the workaround for new T()?
Pass a Class<T> token and use reflection.

Q5: Do wildcards exist after erasure?
No, only List remains at runtime.

Q6: How does reflection interact with erasure?
It can retrieve generic signatures but not enforce them.

Q7: What’s the difference between reifiable and non-reifiable types?
Reifiable types exist at runtime, non-reifiable lose type info.

Q8: Does type erasure affect performance?
No, generics are compile-time only.

Q9: Can arrays hold generics safely?
No, arrays are covariant and unsafe with erased generics.

Q10: Will Java ever support reified generics?
Possibly in the future, but currently erasure is the standard.