Back to Blog
Java

Understanding Java Generic Type Erasure

Java erases generic type parameters at compile time, which affects instanceof, reflection, and arrays of parameterized types. See how bridge methods and heap pollution behave and how type tokens can preserve runtime type information.

Java genericstype erasureruntime type informationbridge methodsheap pollution
Diagram illustrating Java generic type erasure where type parameters are replaced with Object at compile time.

Java generic type erasure is the process by which the compiler removes generic type parameters and replaces them with their leftmost bound, or with Object when no bound is declared. The type information is available during compilation but not in the bytecode at runtime. Knowing this explains why instanceof cannot be used with parameterized types, why reflection cannot recover the type argument from an object instance, and why unchecked casts are sometimes necessary.

How Java Generics Are Compiled

Java generics are a compile-time feature. When you write List<String>, the compiler checks that you only add strings and that you assign the list to a variable of the correct type. The bytecode that runs on the JVM has no concept of generic type parameters. The compiler applies type erasure: it replaces type parameters with their leftmost bound, or with Object if no bound is declared. So List<String> becomes a raw List at runtime, and the element type is effectively Object.

That is the core of type erasure. It explains why instanceof cannot be used with a parameterized type, why reflection cannot see generic arguments on an object instance, and why unchecked casts are sometimes necessary.

The Compiler's Role: Replacing Type Parameters

Consider a simple generic class:

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

After compilation, the type parameter T is erased to its bound. Because T has no explicit bound, it becomes Object. The compiled class is effectively:

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

If T is bounded, such as T extends Number, then T is replaced with Number. The compiler also inserts casts where needed. When you call box.get() and assign the result to an Integer, the compiler inserts a cast to Integer at the call site. The bytecode contains a checkcast instruction.

Runtime Consequences: Why Type Information Is Lost

Because type parameters are erased, the JVM has no knowledge of the actual type argument used when an object was created. This leads to several limitations:

  • instanceof cannot be used with parameterized types. if (list instanceof List<String>) is a compile error because the runtime cannot distinguish List<String> from List<Integer>.
  • Reflection cannot retrieve generic type arguments from an object instance. You can inspect generic declarations on fields and methods in source code: Field.getGenericType() returns the declared generic type of a field, while Method.getGenericReturnType() and Method.getGenericParameterTypes() return generic information for methods. These APIs do not recover the type argument of an object that was created at runtime.
  • You cannot create arrays of parameterized types, such as new List<String>[10], because the array would need to store type information that erasure removes.

These limitations are direct consequences of type erasure. They affect API design and what you can do with reflection.

Bridge Methods and Synthetic Code

When a generic class is extended with a specific type argument, the compiler may generate synthetic bridge methods to preserve polymorphism. For example:

public class StringBox extends Box<String> { @Override public void set(String value) { super.set(value); } @Override public String get() { return super.get(); } }

The erased Box class has a set(Object) method. The StringBox class overrides set(String). For the override to work with the erased signature, the compiler generates a bridge method set(Object) that casts its argument to String and delegates to set(String). Similarly, a bridge method Object get() calls String get() and returns the result. These synthetic methods appear in bytecode but not in source code. They ensure that calling the method through a Box reference still works correctly.

Heap Pollution and Unchecked Warnings

Mixing raw types with generics can cause heap pollution, where a variable of a parameterized type points to an object that contains elements of a different type. Consider:

List rawList = new ArrayList(); List<String> strings = rawList; // unchecked warning rawList.add(42); String value = strings.get(0); // ClassCastException at runtime

The compiler warns about the unchecked assignment. Because of erasure, the runtime does not know that rawList is meant to contain strings. The cast inserted at strings.get(0) fails when the element is an Integer. Heap pollution is a runtime risk that exists only because type information is erased. Avoid raw types in new code and treat unchecked warnings seriously.

Working Around Type Erasure: Type Tokens and Class References

To preserve type information across method boundaries, you can pass a Class object that represents the type. This is often called a type token. For example, when building a generic DAO or a JSON deserializer, you might need the runtime class to perform reflection or instantiate objects:

public <T> T deserialize(String json, Class<T> clazz) { // use clazz to create an instance or inspect fields }

Callers pass MyClass.class, and the method can use clazz to get constructors, fields, or annotations. This pattern is common in libraries such as Jackson and Gson. A Class token is not sufficient for a parameterized type such as List<String>, though. For those cases, Guava's TypeToken uses an anonymous subclass to capture the generic superclass type at compile time. The anonymous class retains the generic signature in its bytecode, which reflection can read.

Performance and Maintainability Considerations

Because type erasure removes generic type parameters, generic code does not keep separate runtime metadata for each type parameter. The main runtime effects are the checkcast instructions inserted by the compiler and the bridge methods generated for polymorphic overrides. These overheads are usually negligible, but they can matter in extremely hot paths, so measure if you suspect they do.

The larger concern is maintainability. Erasure makes it easy to write code that compiles but fails at runtime with ClassCastException. You lose compile-time safety when you mix raw types or use reflection. To mitigate this, keep generics in your public APIs, avoid raw types, and use type tokens when you need runtime type information. These practices reduce the risk of heap pollution and make the code easier to reason about.

Java Generic Type Erasure: How It Works and How to Work Around It | RYUSLOG DEV