Back to Generics & Type Safety
Q3Question 3 of 7

What is type erasure?

Key Concepts
  • Type erasure means the Java compiler removes all generic type parameters after compile-time checking, replacing them with their bounds or Object.
  • At runtime, List<String> and List<Integer> are the same class (ArrayList), with no way to distinguish them.
  • You cannot write new T() because the JVM does not know what T is at runtime; the workaround is passing Class<T> and using reflection.
  • You cannot write instanceof List<String> because type parameters are erased; use instanceof List<?> with unbounded wildcard instead.
  • You cannot create a generic exception class (class MyException<T> extends Exception) because catch clauses need concrete types.
  • Bridge methods are synthetic methods the compiler generates to preserve polymorphism after erasure, bridging erased and specific signatures.
  • Type erasure was chosen for backward compatibility with pre-Java-5 code: generic and raw collections interoperate seamlessly.

Definition

Type erasure means the Java compiler enforces generic type safety at compile time and then removes all type parameters from the bytecode, so at runtime List<String> and List<Integer> are both just List.
  • Erasure process – the compiler replaces T with Object (or the upper bound if bounded: T extends Number becomes Number), inserts casts where needed, and generates bridge methods
  • Same class at runtime – List and List are both ArrayList at runtime; list.getClass() returns the same Class object
  • Restrictions – no new T(), no instanceof T, no T.class, no generic exception types, no generic array creation (new T[])
  • Bridge methods – synthetic methods generated to maintain polymorphism when a subclass narrows a type parameter; they delegate from the erased signature to the specific one
  • Backward compatibility – erasure ensures that generic code interoperates with pre-Java-5 raw type code without breaking existing bytecode

Generics are like coloured labels on filing drawers at National Bank of India during work hours (compile time). The green label says "Savings accounts only" and the red label says "Loan documents only". The compiler uses these labels to prevent Rajesh from putting a loan document in the savings drawer. But at the end of the day (runtime), the cleaning crew rips off all labels (erasure). Every drawer just says "Files". You cannot ask "Is this the savings drawer?" because the label is gone.

Understanding the Concepts

What is type erasure?

Type erasure is the process by which the Java compiler removes all generic type information after performing compile-time type checking. The resulting bytecode contains no traces of type parameters. This was a deliberate design choice for backward compatibility with pre-Java-5 code.

JAVA
package com.bank.demo;

import java.util.ArrayList;
import java.util.List;

public class TypeErasureDemo {
    public static void main(String[] args) {
        List<String> accounts = new ArrayList<>();
        List<Integer> amounts = new ArrayList<>();

        // At runtime, both are the same class
        System.out.println("accounts class: " + accounts.getClass().getName());
        System.out.println("amounts class: " + amounts.getClass().getName());
        System.out.println("Same class: " + (accounts.getClass() == amounts.getClass()));
    }
}
TEXT
accounts class: java.util.ArrayList
amounts class: java.util.ArrayList
Same class: true

How does the compiler erase type parameters?

The compiler replaces each type parameter with its bound. If the type parameter is unbounded (), it becomes Object. If it has a bound (), it becomes Number. The compiler also inserts casts at call sites.

JAVA
// SOURCE CODE:
public class AccountBox<T> {
    private T account;
    public T getAccount() { return account; }
    public void setAccount(T account) { this.account = account; }
}

// AFTER ERASURE (bytecode equivalent):
public class AccountBox {
    private Object account;
    public Object getAccount() { return account; }
    public void setAccount(Object account) { this.account = account; }
}

// SOURCE CODE with bound:
public class NumberBox<T extends Number> {
    private T value;
    public T getValue() { return value; }
}

// AFTER ERASURE:
public class NumberBox {
    private Number value;   // T replaced with Number (the bound)
    public Number getValue() { return value; }
}

At the call site, the compiler inserts a cast:

JAVA
// Source:
AccountBox<String> box = new AccountBox<>();
String name = box.getAccount();

// Bytecode equivalent:
AccountBox box = new AccountBox();
String name = (String) box.getAccount(); // compiler-inserted cast

What can you NOT do because of type erasure?

Type erasure creates several restrictions because the JVM has no knowledge of generic types at runtime.

JAVA
package com.bank.demo;

import java.util.List;

public class ErasureRestrictionsDemo {

    // RESTRICTION 1: Cannot instantiate T
    static <T> T createInstance() {
        // return new T(); // COMPILE ERROR: cannot instantiate type parameter
        return null;
    }

    // WORKAROUND: pass Class<T>
    static <T> T createInstance(Class<T> clazz) throws Exception {
        return clazz.getDeclaredConstructor().newInstance();
    }

    // RESTRICTION 2: Cannot use instanceof with parameterised type
    static void checkType(Object obj) {
        // if (obj instanceof List<String>) {} // COMPILE ERROR
        if (obj instanceof List<?>) { // OK: unbounded wildcard
            System.out.println("It is a List of something");
        }
    }

    // RESTRICTION 3: Cannot create generic array
    static <T> void createArray() {
        // T[] arr = new T[10]; // COMPILE ERROR
        // List<String>[] arr = new List<String>[10]; // COMPILE ERROR
    }

    // RESTRICTION 4: Cannot create generic exception
    // class BankException<T> extends Exception {} // COMPILE ERROR

    public static void main(String[] args) throws Exception {
        // Workaround for new T()
        String instance = createInstance(String.class);
        System.out.println("Created: '" + instance + "'");

        checkType(List.of("SAV2001"));
    }
}
TEXT
Created: ''
It is a List of something

What are bridge methods?

Bridge methods are synthetic methods the compiler generates to maintain polymorphism after erasure. When a class implements a generic interface and narrows the type parameter, the erased interface method has a different signature than the class method. The compiler generates a bridge that delegates.

JAVA
package com.bank.demo;

import java.lang.reflect.Method;
import java.util.Arrays;

public class BridgeMethodDemo {

    interface Comparable<T> {
        int compareTo(T o);
    }

    // After erasure: interface Comparable { int compareTo(Object o); }

    static class Account implements Comparable<Account> {
        String id;
        Account(String id) { this.id = id; }

        @Override
        public int compareTo(Account other) {
            return this.id.compareTo(other.id);
        }
        // Compiler generates bridge method:
        // public int compareTo(Object o) { return compareTo((Account) o); }
    }

    public static void main(String[] args) {
        // Show that the bridge method exists via reflection
        Method[] methods = Account.class.getDeclaredMethods();
        for (Method m : methods) {
            if (m.getName().equals("compareTo")) {
                System.out.println(m + " | bridge: " + m.isBridge());
            }
        }

        Account a = new Account("SAV2001");
        Account b = new Account("CUR3001");
        System.out.println("compareTo: " + a.compareTo(b));
    }
}
TEXT
public int BridgeMethodDemo$Account.compareTo(BridgeMethodDemo$Account) | bridge: false
public int BridgeMethodDemo$Account.compareTo(java.lang.Object) | bridge: true
compareTo: 1

How is this different from C# reified generics?

| Feature | Java (erased) | C# (reified) |
|---|---|---|
| Type info at runtime | Not available | Available |
| new T() | Not possible | Possible with new() constraint |
| typeof(List) | Not possible | Possible |
| Runtime type check | instanceof List only | typeof(List) works |
| Backward compat | Seamless with raw types | Required new runtime |

What are common mistakes?

Mistake: Trying to create instances of T

JAVA
// WRONG: T is erased at runtime
public class Repository<T> {
    public T createEmpty() {
        return new T(); // COMPILE ERROR
    }
}
JAVA
// CORRECT: pass Class<T> and use reflection
public class Repository<T> {
    private final Class<T> entityClass;
    public Repository(Class<T> entityClass) { this.entityClass = entityClass; }
    public T createEmpty() throws Exception {
        return entityClass.getDeclaredConstructor().newInstance();
    }
}

Mistake: Using instanceof with parameterised types

JAVA
// WRONG
if (obj instanceof List<String>) { } // COMPILE ERROR
JAVA
// CORRECT: use unbounded wildcard
if (obj instanceof List<?> list) { // pattern matching (Java 16+)
    System.out.println("List of size: " + list.size());
}

What should a senior engineer know?

  • Reifiable vs non-reifiable types. A reifiable type is one whose type information is fully available at runtime (String, List, int[]). A non-reifiable type has some information erased (List, Map). Arrays require reifiable component types, which is why new List[10] is illegal.
  • Super type tokens. Frameworks like Jackson and Gson use TypeReference> or TypeToken to capture generic type information via anonymous subclasses, working around erasure.
  • Valhalla project. Project Valhalla aims to bring specialised generics to Java, where List would store primitives directly without boxing, partially addressing erasure limitations.

What are common interview follow-up questions?

  • "Why did Java choose type erasure?" : For backward compatibility with pre-Java-5 code. Existing bytecode and libraries continued to work without changes.
  • "Can you determine the generic type at runtime?" : Not directly. You can use super type tokens (anonymous subclass trick) or pass Class explicitly.
  • "What is a bridge method?" : A synthetic method generated by the compiler to bridge the erased signature and the specific signature, preserving polymorphism.
  • "Why can you not create a generic array?" : Arrays are reified (know their component type at runtime) and throw ArrayStoreException on type violations. Generic arrays would break this guarantee because the type is erased.

Visual Overview

DIAGRAM
  Type Erasure Process
  =====================

  Source code:                     Bytecode (after erasure):
  ┌──────────────────────┐         ┌──────────────────────┐
  │ class Box<T> {       │         │ class Box {           │
  │   T item;            │ ──────▶ │   Object item;       │
  │   T get() {}         │         │   Object get() {}    │
  │   void set(T t) {}   │         │   void set(Object) {}│
  │ }                    │         │ }                    │
  └──────────────────────┘         └──────────────────────┘

  Box<String> b = new Box<>();     Box b = new Box();
  String s = b.get();              String s = (String) b.get();
                                   // compiler inserts cast

  Bridge Method:
  ┌──────────────────────────────────────────────────────┐
  │ interface Comparable<T> { int compareTo(T o); }      │
  │ After erasure: int compareTo(Object o)               │
  │                                                       │
  │ class Account implements Comparable<Account> {        │
  │   int compareTo(Account o) { ... }                   │
  │   int compareTo(Object o) { return compareTo((Account)o); } │
  │                            ^^^^ bridge method (synthetic)    │
  └──────────────────────────────────────────────────────┘

Key Takeaway

Type erasure is the mechanism by which Java generics provide compile-time type safety without changing the runtime bytecode format. The compiler checks all type constraints, inserts necessary casts, and then removes all type parameters. At runtime, List and List are the same class, and there is no way to distinguish them.

This creates several restrictions: you cannot write new T() (the JVM does not know what T is), you cannot use instanceof with parameterised types, you cannot create generic arrays, and you cannot create generic exception classes. The workaround for new T() is to pass Class and use reflection. Bridge methods are generated automatically to preserve polymorphism when a class narrows a generic type parameter.

At National Bank of India, Rajesh's generic Repository pattern could not do new T() to create empty entity instances. Instead, the repository constructor accepted Class, and the factory method used reflection. This is the standard pattern used by JPA repositories, Spring Data, and similar frameworks.

Remember one sentence: type erasure removes all generic type information at compile time for backward compatibility, making List and List the same class at runtime, which is why you cannot do new T() or instanceof List.

---

Share
Use ← → arrow keys to navigate