Type Erasure in Java Generics

Understand how Java generics disappear at compile time, what erasure does to your types, and the implications for your code.

published: reading time: 16 min read author: Geek Workbench

Type Erasure in Java Generics

Type erasure is the mechanism by which Java implements generics. At compile time, all generic type information (<T>, <String>) is removed — replaced by their erased types (typically Object or the leftmost bound). The JVM has no knowledge of generics at runtime. This was a deliberate design choice made in Java 1.5 to maintain binary compatibility with pre-generics code.

What Gets Erased

| Generic Declaration | Erasure |

| --------------------------- | -------------------------------------------------- |

| <T> | Object |

| <T extends Number> | Number |

| <T extends Comparable<T>> | Comparable (the bound, after its own erasure) |

| <K, V> (multiple) | Each erased independently per its own bound |

| List<String> | List (raw type) |

| List<? extends Number> | List (raw type, with wildcard flag for compiler) |

After erasure, the compiled bytecode is identical to pre-generics Java code that used Object casts throughout.

Code Example: Before and After Erasure


// SOURCE CODE

public class Container<T> {

    private T value;

    public T get() { return value; }

    public void set(T value) { this.value = value; }

}

// COMPILED BYTECODE (erasure applied)

public class Container {

    private Object value;  // T replaced with Object

    public Object get() { return value; }

    public void set(Object value) { this.value = value; }

}

The same happens at call sites:


// SOURCE

Container<String> box = new Container<>();

box.set("Hello");



// COMPILED

Container box = new Container();

box.set("Hello"); // "Hello" is already an Object — no cast needed

// retrieval: String s = box.get();

// compiler inserts: String s = (String) box.get();

// Erasure + cast insertion = what the compiler emits

Mermaid Diagram: Erasure Process — Compile Time


flowchart TD

    A["public class Box&lt;T&gt;\n{\n  T value;\n  T get() { return value; }\n}"] --> B["Type Checking"]

    B --> C["Erasure"]

    C --> D["public class Box\n{\n  Object value;\n  Object get() { return value; }\n}"]

    D --> E["Insert Casts"]

    E --> F["Bytecode: Box.java"]

Runtime


flowchart TD

    G["Loading Box.class"]

    G --> H["No generic signature retained\n— just raw Box class"]

Code Example: Bounded Type Erasure


// SOURCE

public class NumericBox<T extends Number> {

    private T value;

    public double compute() {

        return value.doubleValue(); // calling Number's method

    }

}



// ERASED

public class NumericBox {

    private Number value;  // T replaced with leftmost bound: Number

    public double compute() {

        return value.doubleValue(); // still valid — Number has doubleValue()

    }

}

The bound Number is kept after erasure because it is the type that provides the methods you call.

Failure Scenarios

1. Cannot Instantiate T


public <T> void createInstance(Class<T> clazz) {

    // T t = new T(); // compile error: cannot do new T

    // Erasure makes T = Object at runtime, so new Object() would be wrong type

    T instance = clazz.getDeclaredConstructor().newInstance(); // workaround

}

2. Cannot Create Generic Arrays Directly


public <T> void wrongArray() {

    // T[] arr = new T[10]; // compile error

    // Fix: use Array.newInstance()

    T[] arr = (T[]) Array.newInstance(Object.class, 10); // unchecked cast

}

3. Collision After Erasure


// Two methods with the same erasure — compile error

public class Colliding {

    // public void set(T value) { }

    // public void set(List<T> list) { } — both erase to set(Object)

}

4. Cannot Call Class methods on T at Runtime


public <T> void example(T param) {

    // Class<?> c = param.getClass(); // works — getClass() is on Object

    // But Class<T> methods that need T at runtime are unsafe

    // T.class cannot be used: Class<T> does not give you .class at runtime

}

Trade-Off Table

| Concern | Impact of Erasure | Mitigation |

| -------------------------- | ------------------------------------------ | ----------------------------------------------------- |

| instanceof with generics | Illegal — no runtime type info | Use alternative: markers, separate methods |

| new T() | Not possible | Use Class<T>.newInstance() or Array.newInstance() |

| T.class | Not valid syntax | Pass Class<T> as parameter |

| Array creation | No direct new T[] | Use Array.newInstance() with unchecked cast |

| Bridge methods | Compiler adds to preserve overrides | Be aware when debugging bytecode |

| Binary compatibility | Pre-generics code works with generics code | No breaking changes to existing JARs |

Observability Checklist

  • Use javap -c on compiled classes to see actual erased signatures and bridge methods

  • Check for “unchecked” warnings in compilation output — these signal erasure-related unsafe operations

  • Verify framework serialization does not rely on generic type parameters (most handle erasure via TypeToken / custom serializers)

  • Test with pre-generics code paths if maintaining binary compatibility with older JARs

  • Use javap -v for full constant pool and generic attribute debugging

Security Notes

  • No runtime type enforcement: List<String> and List<Integer> are both just List at runtime. Malicious code that bypasses generics can inject the wrong type. Validate at boundaries.

  • Unsafe casts: Erasure means the compiler inserts casts. If you use raw types or @SuppressWarnings("unchecked"), you bypass these safety nets. Audit for raw type usage in security-critical paths.

  • Reflection exposure: Reflection APIs like Field.getGenericType() return Type objects that include generic info from source, but the actual field in the class file is erased. Do not use generic type info for security decisions.

Pitfalls

  1. Class<T> does not retain T at runtime: List<String>.class is illegal. You must pass a Class<String> token explicitly if you need reifiable type information at runtime.

  2. Varargs and heap pollution: List<String>[] varargs can cause heap pollution because the array type is erased. @SafeVarargs suppresses the warning but does not fix the issue.

  3. Confusing errors when bounds are missing: If you call a method on T and it is not in the bound, the compile error references erasure — not the original type parameter.

  4. Generic enums with inheritance: Enum classes cannot extend other types, and generic enum declarations are not allowed because enums inherit java.lang.Enum which is already parameterized.

Quick Recap

  • Type erasure removes all generic type information at compile time

  • Unbounded <T> becomes Object; bounded <T extends X> becomes X

  • The compiler inserts casts at retrieval points and generates bridge methods where needed

  • Runtime sees raw types only — List<String> and List<Integer> are identical at runtime

  • Erasure is the reason you cannot new T(), use T.class, or instanceof List<String>

  • Erasure was chosen to maintain backward binary compatibility with Java 1.4 and earlier

Interview Questions

::: info

These questions use the .qa-card CSS class structure. Each card has a .qa-question and .qa-answer div.

:::

1. What is type erasure in Java generics?

Model Answer: "Type erasure is the process by which the Java compiler removes all generic type information (``, ``) at compile time. Unbounded type parameters are replaced with `Object`; bounded type parameters are replaced with their leftmost bound. The result is that at runtime, a `List` and `List` are both just a raw `List` — no type argument information is retained."

2. Why did Java choose erasure over reified generics?

Model Answer: "Java 1.5 introduced generics without breaking binary compatibility with pre-generics code. With reifiable generics (like C#'s), the generic type is retained at runtime. With erasure, existing compiled `.class` files that used raw types or `Object` casts continue to work with new generic code. The JVM did not need to change; the language did. This was a pragmatic trade-off for adoption."

3. Why cannot you instantiate a new T() in a generic class?

Model Answer: "Because after erasure, `T` becomes `Object`, and `new Object()` is not the right type. At runtime, there is no way to know what `T` was originally — no token, no class reference. The standard workaround is to pass a `Class` object and use `clazz.newInstance()` or `Array.newInstance()`, which reifies the type at runtime via the caller's token."

4. How does the bound affect erasure of a type parameter?

Model Answer: "The bound is used as the erasure replacement for the type parameter: `` becomes `Number`, not `Object`. This is why you can call `Number`-specific methods in a generic class bounded by `Number`. If there are multiple bounds like `>`, the leftmost bound (`Number`) is used for the field type, while the others are only recorded as flags in the generic signature attribute for the compiler's use."

5. What is the performance impact of type erasure at runtime?

Model Answer: "No meaningful effect. After erasure, bytecode is nearly identical to hand-written Object-and-cast code, which the JIT has been optimizing for decades. The generic type checks happen at compile time; the runtime penalty is zero. The JIT sees the erased types and can inline and optimize normally."

6. What is the generic signature attribute in class files?

Model Answer: "The generic signature attribute is metadata stored in the class file (not in the bytecode itself) that records the generic type information from the source code. It is used only by the compiler — the JVM ignores it at runtime. For example, `Pair` stores the generic signature `Pair` in the class file so the compiler can verify type safety at call sites. Tools like `javap -v` show the signature attribute. It does not affect runtime behavior — erasure still applies."

7. Can two different generic types have the same erasure?

Model Answer: "Yes. For example, `class Container` and `class Box` both erase to their raw form `Container` and `Box`. But within the same class, `List` and `List` both erase to `List` — the type argument is lost. More subtly, `` and `>` both use `Number` and `Comparable` as their erased types respectively. Erasure can also cause method signature collisions when two methods in the same class have the same erasure (e.g., `void set(T value)` and `void set(List list)` both become `void set(Object)`)."

8. What is heap pollution and how does it relate to erasure?

Model Answer: "Heap pollution occurs when a variable of a parameterized type refers to an object that is not of that type. With generics, it happens when varargs on a generic type creates an array that the compiler cannot verify at runtime: `List[] array = new List[5];` — the array type carries generic info at compile time but the runtime type is `Object[]`. Mixing raw types and parameterized types in varargs is the primary source. The compiler warns but `@SafeVarargs` silences it without fixing the underlying issue."

9. How does reflection interact with generic type information after erasure?

Model Answer: "Reflection sees raw types — `Field.getType()` returns `Object` for a field of type `T` after erasure. `Field.getGenericType()` returns the generic type from the source signature attribute, but this is only metadata the compiler uses, not runtime type information. `Class` does not retain `T` at runtime — `List.class` is illegal. To get type information at runtime, you must pass explicit `Class` tokens or use libraries like Gson that read the generic signature attribute and reconstruct the type via TypeToken."

10. Can overloaded methods have the same erasure?

Model Answer: "Yes, but only if the erased signatures differ. ` void process(T item)` and ` void process(List list)` both erase to `void process(Object)` and `void process(Object)` — a compile error (name clash). However, ` void process(T num)` and ` void process(T cs)` both use `Number` and `CharSequence` respectively, so they have different erased signatures and can coexist."

11. Why cannot you use instanceof with parameterized types?

Model Answer: "You cannot use `instanceof` with a parameterized type: `obj instanceof List` is a compile error. `instanceof List` (raw type) is legal. After erasure, `List` is just `List` at runtime, so there is no type argument to check. The JVM only knows the raw type from the actual object loaded. If you need runtime type checking with generics, you must pass a `Class` token and check `clazz.isInstance(obj)`."

12. What are the trade-offs of using type erasure in Java?

Model Answer: "Java chose erasure to maintain binary compatibility with pre-generics code (Java 1.4 and earlier). Adding reified generics to the JVM would have required changing the class file format and breaking existing compiled code. With erasure, existing `.class` files using raw types continue to work with new generic code. The trade-off is losing runtime type information, but the language avoids a breaking change to the platform."

13. Why does T super Number not compile in a type parameter declaration?

Model Answer: "`super` is only valid in wildcard usage (`? super T`), not in a type parameter declaration. `` is a compile error. The `super` keyword in generics is used at the call site for lower-bounded wildcards to enable contravariant write flexibility. In a type parameter declaration, only `extends` is valid (upper bound). The lower bound concept exists only in existential type contexts with wildcards."

14. How does erasure work with nested generic types?

Model Answer: "Each level of nesting is erased independently. `Map>` becomes `Map` after erasure (the inner `List` also becomes `List`). The signature attribute records the full generic type for the compiler's use, but at runtime every parameterized type is stripped to its raw form. Nested arrays of generic types (e.g., `List[]`) cause heap pollution because the array type carries type information that is not verifiable at runtime."

15. How does type erasure affect lambda expressions?

Model Answer: "Lambdas can be generic: `Function func = (t) -> someExpression` — the compiler infers `T` and `R` from the target type. At runtime, lambdas are invoked via `invokedynamic` and the generic signature of the lambda's method is erased. However, the lambda body itself only uses the erased types. If the lambda uses a generic type parameter from an enclosing method, the lambda captures that type — but after erasure, it is as if the lambda worked with `Object` or the bound type."

16. How does Java serialization handle generic types after erasure?

Model Answer: "Not natively with standard Java serialization. `ObjectOutputStream` writes raw types — `List` and `List` are both serialized as `List`. Use libraries that handle generic types via `TypeToken` (Gson), `TypeReference` (Jackson), or custom `TypeAdapter` implementations that read the generic signature attribute to reconstruct parameterized types at runtime. Standard Java serialization provides no built-in mechanism for preserving generic type arguments."

17. What are bridge methods and how do they relate to erasure?

Model Answer: "After erasure, a generic method `T get()` in a superclass becomes `Object get()`. If a subclass overrides it with `String get()`, the signatures do not match. Bridge methods solve this by generating `Object get()` in the subclass that delegates to `String get()`. Without bridges, the override would not be valid after erasure, and polymorphic calls would call the wrong method. Bridge methods restore the override relationship at the cost of synthetic methods in bytecode."

18. Why does the compiler insert casts after type erasure?

Model Answer: "Because after erasure, method return types are the erased types (e.g., `Object` instead of `T`). At a call site like `String s = box.get()`, the compiler must insert a cast to verify that the retrieved object is actually a `String`. Without the cast, assigning `box.get()` to `String` would be unsafe. Erasure plus cast insertion equals what makes generic code type-safe at compile time while producing pre-generics-compatible bytecode."

19. Why is List.class illegal in Java?

Model Answer: "`List.class` is illegal because the generic type is erased at runtime — there is no `List` class, only `List`. The workaround is `new com.google.gson.reflect.TypeToken>(){}` (anonymous subclass of TypeToken) or passing `Class>` directly. Some frameworks (like Spring) use `Class.forName()` with parameterized types to resolve generic type information at runtime via the generic signature attribute."

20. What is the relationship between type inference and type erasure?

Model Answer: "Type inference is the compiler's ability to deduce type arguments at the call site. Type erasure is what happens to those types after inference — they are stripped. The two are complementary: inference determines what `T` is at compile time; erasure removes `T` at compile time for the bytecode. After erasure, the inferred types no longer exist — they are replaced with `Object` or a bound. The result is that inference works hard at compile time only for the types to be immediately erased when the bytecode is generated."


Further Reading


Summary

Type erasure is the mechanism that makes Java generics work without changing the JVM. At compile time, all generic type information is removed — unbounded T becomes Object, bounded T extends Number becomes Number. The compiler also inserts the casts that would otherwise be written by hand. At runtime, there is no List<String> — only a raw List.

The consequences of erasure shape everything you cannot do with Java generics. No new T(), no T.class, no instanceof List<String>. These are not arbitrary restrictions — they are the natural result of erasing type information before the bytecode is loaded. The JVM simply does not know what T was.

What many miss is that erasure also affects bounds. <T extends Number & Comparable<T>> erases to Number — not Comparable. The secondary bound (Comparable<T>) is recorded in the class file’s generic signature attribute for the compiler’s use, but the field and method signatures use only the first bound’s type. This has real implications when crossing into reflection or dealing with complex generic inheritance chains.

Bridge methods are the direct consequence of erasure. When a subtype overrides a generic method with a more specific return type, the signatures no longer match after erasure. The bridge method restores the override relationship: Object get() delegates to String get(). See Erasure and Bridge Methods in Java for the full mechanics.


Category

Related Posts

Abstract Classes in Java

Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.

#java-abstract-classes #java #java-fundamentals

Arithmetic Operators in Java

Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.

#java-arithmetic-operators #java #java-fundamentals

Array Basics in Java

Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.

#java-array-basics #java #java-fundamentals