Type Erasure in Java

Type-Erasure-in-Java-feature.jpg

When the Java code is compiled, before generating the bytecode, the Java compiler performs type erasure in Java generics. This means that the type parameters of the generics are removed, and type safety is used at compile time. As a result, only the raw types remain in the bytecode, and generic type information is not available at runtime.

In this blog, we will discuss the Java generics type erasure, with its types, effects, limitations, best practices, and real-world use cases in detail.

Table of Contents:

What are Java Generics?

Generics in Java allow creating classes, interfaces, and methods with type parameters, and ensure type safety and reduce the need for type casting in Java generics. They also help in writing reusable, maintainable, and efficient code by defining a placeholder for a type instead of using the raw types like Objects.

Before generics, Java used raw data types like List and ArrayList, in which storing any object leads to runtime errors due to data type differences. Generics solve this problem by using type safety at compile time.

With generics, you can define a type parameter, which will ensure that only one type of object can be stored in it. This will remove the need for explicit type casting in Java generics and will also prevent runtime errors.

Generics in Java are similar to the templates in C++, but they differ in implementation. There are two types of Java generics. These are:

  • Generic Methods
  • Generic Classes

What Is Type Erasure in Java?

In Java, type erasure refers to the removal of all generic type information by the Java compiler. During the compilation of generic code, the Java compiler verifies the type constraints of code (this is done to provide type safety). Then, the compiler replaces the type parameters with their bounds (or Object if unbounded), and after type erasure, the resulting compiled file contains no generic-type information. At runtime, the compiled code operates with only raw types.

The primary intent of type erasure in Java is to ensure backward compatibility with previous Java versions that did not support generics. Type erasure must occur; otherwise, classes and methods that use generics will create a new copy for each parameter type. Instead, the Java compiler compiles classes and methods that are based on generics into a single version or class, and the code works with raw types. As a result, the newer generic code can work correctly with legacy non-generic code.

List<String> list = new ArrayList<>();
list.add("Hello");
// At runtime, treated as: List list = new ArrayList();

In this example, the compiler enforces type safety (ensuring only String values can be added) at compile time. But after compilation, the type information (<String>) is erased, and the runtime sees only a raw List.

Why Does Java Use Type Erasure?

This ensures compatibility with older code and also prevents the creation of new classes having different generic types.

Generics were introduced to the Java language to provide type checks at compile time and to support generic programming.

Type erasure in Java ensures that no new classes are created for the parameterized types, i.e., it ensures that the bytecode generated by the compiler is compatible with the existing code that does not use Generics. Type erasure converts parameterized types into raw types in Java, allowing backward compatibility with legacy code. Understanding raw types in Java helps to explain the consequences of type erasure and why runtime type checks for generics are limited.

How Type Erasure Works in Java

When using generics, the Java compiler applies type erasure to:

  • Generate bridge methods to preserve polymorphism in extended generic types.
  • Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods.
  • Insert type casts if necessary to preserve type safety.

Bridge Methods in Java: What They Are and Why They Matter

Bridge methods are automatically generated by the Java compiler to maintain polymorphism and type safety when using generics. They also ensure that the code that uses generics remains compatible with the older versions of Java that do not support generics.

When a subclass overrides a method from a generic superclass, type erasure in Java can cause a problem in the method signatures. The compiler creates a bridge method to ensure that both the generic and erased versions of the method work correctly.

Example of Bridge Method Generation 

1. Before Type Erasure (Generic Version)

class Parent<T> {
    T data;
    void setData(T data) {
        this.data = data;
    }
}class Child extends Parent<String> {
    void setData(String data) {  // Overrides Parent<T>'s method
        this.data = data;    
}
}

2. After Type Erasure (Generated Bridge Method in Child Class)

class Child extends Parent {
    void setData(String data) { 
        this.data = data;
    }       // Compiler-generated bridge method
    void setData(Object data) { 
        setData((String) data);  // Calls the overridden method
    }
}

Reifiable vs Non-Reifiable Types in Java

A Reifiable type is a type whose type information is fully available at runtime. This includes primitives, non-generic types, raw types, and invocations of unbound wildcards. 

A Non-Reifiable type is a generic type whose type information is erased at runtime due to type erasure in Java. This means the Java Virtual Machine (JVM) does not keep complete type details, treating List<String> and List<Integer> as just List.

Since type information is not available at runtime,  some operations are not allowed, such as using instanceof with generics. This can lead to runtime errors if unchecked casts are used. Also, the concept of raw types in Java shows the outcome of type erasure in Java; generic classes lose type parameters after compilation.

Example:

Java

Output:

Non-Reifiable type

Explanation: The code is showing that List<String> and List<Integer> are treated the same at runtime. This is happening because Java is removing the generic type details. So, both lists are being considered as List and stringList.getClass() == integerList.getClass() is returning true.

Different Types of Type Erasure in Java

Type erasure in Java occurs in three main ways:

  • Type Parameter Erasure (Class Level Erasure)
  • Method Type Erasure
  • Type Erasure in Wildcards (?)
Master Java Today - Accelerate Your Future
Enroll Now and Transform Your Future
quiz-icon

1. Class-Level Type Erasure in Java Generics

When a generic class or interface is compiled, type parameters are removed and replaced with either their first bound type (if specified) or with their object (if no bound is provided).

Example 1: Unbounded Type Parameter (T)

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

After Type Erasure:

class Box {  
    private Object value;  // T is replaced with Object
    public void setValue(Object value) {
        this.value = value;
    }    public Object getValue() {
        return value;
    }
}

In Java, if a class is using a generic type (T) without bounds, the compiler will replace T with Object while compiling. This enables the code to be used by older Java versions that are not compatible with generics. All methods are referencing T as an Object after the type erasure, so there is only type safety at compile time. When the values are extracted, explicit type casting in Java generics is required at runtime. Since T is not bounded, it is replaced with the Object.

Example 2: Bounded Type Parameter (T extends Number)

If a bound is specified in the class, T is replaced with its first bound instead of the Object.

class Box<T extends Number> {
    private T value;
}

After Type Erasure:

class Box {
    private Number value;  // T is replaced with Number
}

In Java, if T extends a Number, the compiler replaces T with the Number instead of the Object. This ensures that only the numbers can be used. After the type erasure in Java, all the methods and the fields using T now can use the Number, which keeps the type safety for the numeric values.

2. Method-Level Type Erasure with Java Generics

When a method has a generic type parameter, it goes through the type erasure in the same way as the class-level erasure.

Example 1: Unbounded Generic Method

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

After Type Erasure:

class Utility {
    public static void print(Object data) {  // T is erased to Object
        System.out.println(data);
    }
}

In an unbounded generic method, the compiler replaces T with Object during type erasure. This makes the method work with any data type and also keeps it compatible with older Java versions. After erasure, the method behaves like it has used an Object instead of a generic type. Since T has no limits, it gets replaced with Object.

Example 2: Bounded Generic Method (T extends Number)

class MathUtils {
    public static <T extends Number> double square(T num) {
        return num.doubleValue() * num.doubleValue();
    }
}

After Type Erasure:

class MathUtils {
    public static double square(Number num) {  // T is erased to Number
        return num.doubleValue() * num.doubleValue();
    }
}

In a bounded generic method, T extends Number, which means that T can only be a type of Number. During type erasure, the compiler replaces T with Number. This allows the method to work with different number types, like Integer or Double. After erasure, the method takes a Number as a parameter.

3. How Type Erasure Affects Java Wildcards

In Java generics, wildcards (?) are used to represent an unknown type. However, due to the type erasure, the information of the wildcard type is removed at runtime, which affects how Java will compile and enforces type safety.

Example 1: Unbounded Wildcard (<?>)

class Printer {
    public static void printList(List<?> list) {
        for (Object obj : list) {
            System.out.println(obj);
        }
    }
}

After Type Erasure:

class Printer {
    public static void printList(List list) {  // ? is erased to raw type List
        for (Object obj : list) {
            System.out.println(obj);
        }
    }
}

In Java, an unbounded wildcard (<?>) is erased during the compilation process, so List<?> becomes a raw List. This makes the method accept any list but loses type safety at runtime. All elements are treated as an Object, which requires explicit type casting in Java generics if needed. 

Example 2: Upper Bounded Wildcard (<? extends Number>)

class Calculator {
    public static void sum(List<? extends Number> numbers) {
        double total = 0;
        for (Number num : numbers) {
            total += num.doubleValue();
        }
    }
}

After Type Erasure:

class Calculator {
  public static void sum(List numbers) {  // ? extends Number is erased to raw List
        double total = 0;
        for (Object num : numbers) {  
            total += ((Number) num).doubleValue();  // Explicit casting required
        }
    }
}

In Java, an unbounded wildcard (<?>) is removed during compilation, changing List<?> to a raw List. This allows passing any list but removes type safety at runtime. All elements become Object, so explicit casting is needed to get values.

Effects of Type Erasure on Java Generics Code

Type erasure in Java has several implications for Generics in Java. Below are the key points to consider:

1. Type Safety

Type erasure in Java ensures type safety at compile time by checking that only the valid types are used in the generic classes and methods. This helps to prevent incorrect type assignments.

However, type safety is not ensured at runtime because type information is removed from the bytecode. After type erasure, generic types like T are replaced with Object. This means that Java cannot check type constraints at runtime, and if incorrect type casting in Java generics is used, it can lead to ClassCastException errors.

Example:

List<String> list = new ArrayList<>();
list.add("Hello");
list.add(123); // Compile-time error (type safety ensured)

If generics didn’t exist, the above code would not give an error at compile time, leading to potential runtime failures. 

2. Performance 

Type erasure in Java does not improve the execution speed of the program, but it keeps the bytecode size smaller by not creating multiple versions of the generic classes.

Example:

class Box<T> {  
    private T value;
}

After type erasure, it becomes:

class Box {  
    private Object value;
}

where T is replaced with the Object, keeping away the separate class versions for the different types like Box<String>, Box<Integer>, etc. 

3. Compatibility with Legacy Code

Type erasure in Java ensures compatibility with older Java versions that do not support generics with the present Object. Since Java replaces generic types with Object, generic code compiles into the same bytecode as non-generic code. Type erasure makes generic types compatible with pre-generics Java by converting them into raw types at compile time.

Example:

List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

Both types of lists, i.e., <String> and <Integer>, are treated as a common List type at runtime, making them compatible with older code that does not use generics. This also means that information of the generic type in Java is lost, so Java cannot find the difference between List<String> and List<Integer> at runtime. 

Limitations of Java Type Erasure

Although type erasure in Java generics provides compatibility, it also introduces some significant type erasure limitations:

1. Cannot Create Instances of Generic Types

Since T is erased, Java does not know its actual type at runtime, so the following is not allowed:

class Box<T> {
    T value = new T();  // Compilation error
}

You can pass the class type as a parameter:

class Box<T> 
{private Class<T> clazz;
    Box(Class<T> clazz) 
{ this.clazz = clazz; 
}    T createInstance() throws InstantiationException, IllegalAccessException {
        return clazz.newInstance();  //  Works
    }
}

2. Cannot Use instanceof with Generics

Java erases generic types, so this check is not possible:

You can use a class reference instead:

if (obj instanceof T) {
//   Compilation error
}

 3. Type Information is Lost at Runtime

Reflection cannot access generic type parameters due to type erasure.

List<String> list = new ArrayList<>();
System.out.println(list.getClass().getTypeName());

You can use TypeToken from Google’s Guava library or explicit class references.

Unlock Your Future in Java
Start Your Java Journey for Free Today
quiz-icon

Heap Pollution in Java and How to Avoid It

Heap Pollution occurs in Java when a variable of a parameterized type (such as List<String>) refers to an object of a different type, which leads to a ClassCastException error at runtime.

This occurs because Java removes generic type information at runtime, treating different parameterized types like List<String> and List<Integer> as the same. It happens when developers use raw types, generic varargs, or unchecked casts. For example, if a raw List is assigned to List<String>, it can allow adding non-string values, which may cause errors later.  

Example:

Java

Output:

Heap-Pollution-in-Java

Explanation: In the above code, when a list meant for Strings (List<String>) is used with a raw type new ArrayList(). Since Java does not keep type details at runtime, it does not prevent adding different types to the list. Later, when Java expects a String but finds a number, it causes a ClassCastException error.

Get 100% Hike!

Master Most in Demand Skills Now!

Type Inference vs Type Erasure in Java

Now learn the key differences between Type Inference and Type Erasure in Java on various aspects like definitions, examples, purpose, limitations, and how they impact generics and code simplicity.

Aspect Type Erasure Type Inference
Definition A process of removing generic type information at compile time A process where the compiler determines the type automatically
When It Occurs During compilation During compilation
Purpose Ensure backward compatibility with legacy Java (pre-generics) Simplify code by reducing the need for explicit type declarations
Applies To Generics Variable declarations, lambda expressions, method calls
Example List<String> -> List at runtime var list = new ArrayList<String>();
Runtime Availability Type information is not available at runtime Inferred type is resolved at compile-time, not retained
Introduced In Java 5 Java 7 (limited), improved in Java 8 and Java 10 (var)
Effect Erases type parameters to raw types Infers type to reduce verbosity
Limitations No access to generic type info at runtime Can reduce readability if overused
Related To Generics implementation Syntax simplification

Type Erasure vs Reified Generics

Here, check out the difference between Type Erasure in Java and Reified Generics in Kotlin, C#, and Scala.

Aspect Type Erasure (Java) Reified Generics (Kotlin, C#, etc.)
Definition Generic type information is removed at compile time Generic type information is preserved at runtime
Runtime Type Info Not available Available
Usage Example List<String> becomes List at runtime List<String> keeps type info at runtime
Allows Type Checks? No Yes
Language Support Java Kotlin, C#, Scala, etc.
Performance Slightly better due to erasure May give overhead, depending on implementation
Limitation Cannot create arrays of generics (new T[]) Can create and use generic arrays (arrayOf<T>())
Safety Less safe, type of information lost More type-safe, the type is known and enforced
Purpose Maintain Java’s backward compatibility Provide safer and more expressive generic programming

Advanced Techniques to Work Around Type Erasure in Java

Although Java’s type erasure removes generic type information at runtime, developers can actively retain or simulate type information by using several techniques, thereby ensuring type safety and flexibility.

1. Using Class Literals (Class<T>)

Pass a Class object to a generic method or constructor to preserve the type. In the example below, clazz serves as a “type token” that keeps some type context at runtime.

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

2. Type Tokens with TypeReference

Libraries like Gson and Jackson use a TypeReference<T> (or similar) to retain generic type information through anonymous subclasses. This allows frameworks to deserialize complex generic types despite type erasure:

Type type = new TypeToken<List<String>>(){}.getType();

3. Java Reflection

Use methods like getGenericSuperclass() or getGenericReturnType() to inspect declared generic types. While not foolproof, reflection can access generic signatures stored in the class metadata:

Type type = MyClass.class.getGenericSuperclass();

4. ParameterizedType in the Reflection API

The java.lang.reflect.ParameterizedType interface can retrieve parameterized type arguments when available.

5. Custom Metadata or Annotations

You can explicitly annotate fields or methods with type information, allowing you to retrieve it at runtime via reflection.

Performance Implications of Type Erasure

From a performance perspective, type erasure is usually neutral because generic type-checking occurs entirely at compile time. Therefore, there are no additional runtime costs associated with parameterized types. The bytecode generated operates on raw types, just like non-generic code does.

That being said, there are some indirect performance considerations:

  • Casting Overhead: The compiler can add casts to a program after erasure for the purpose of type-safety. While these casts cost very little in time, excessive use of casting in performance-critical areas of code could have a slight impact.
  • Bridge Methods: If the compiler generates bridge methods to offer polymorphism of a generic subclass, that may provide some slight overhead in method dispatch. This is rarely noticeable and will only technically account for an additional method call.
  • Reflection Usage: Some of the workarounds for type erasure, such as getting type information from an object using reflection, can be slower than directly using a type. Reflection has its overhead, and even a tiny bit of additional overhead is not ideal.
  • Serialization/Deserialization: Frameworks like Gson or Jackson may need to spend additional processing time reconstructing lost type information due to erasure, which could negatively impact performance in larger data processing tasks.

For the majority of applications, type erasure has no purposeful negative performance implications, and the advantages of generics well outweigh any small runtime costs.

Best Practices for Using Type Erasure in Java

Below, we have listed some of the best practices that you should follow when using Java type erasure.

  • Avoid using raw types and always specify generic types to maintain type safety.
  • You must use bounded type parameters to restrict acceptable types and improve clarity.
  • You can pass Class<T> as a parameter if you need type information at runtime.
  • You shouldn’t use instanceof with generic types; instead, use workarounds like class tokens.
  • Avoid creating generic arrays and use collections or List<T> instead.
  • You must keep generic logic in methods or helper classes to reduce the complexity.
  • Always document your generic APIs well with the help of comments.
  • You can use tools like IDE inspections and static analyzers to find the unsafe generic operations.

Real-world Use Cases of Type Erasure in Java

Here are some real-world use cases of type erasure in Java:

  1. Backward Compatibility: Type erasure in Java makes sure that the generics introduced in Java 5 are compatible with legacy code written before generics existed. And, when Java applies type erasure, it transforms generic classes and interfaces into raw types in Java to maintain legacy compatibility.
  2. Java Collections Framework: The internal implementation of generic collections like ArrayList uses type erasure to store elements as Object, allowing the same code without rewriting logic for each type.
  3. Java Reflection: Because of Java generics type erasure, using the generic type information at runtime is not possible. This is one of the reasons for the reflection API’s inability to extract whatever the actual type parameter T would be, leading to the type erasure limitations in frameworks like Hibernate or Spring.
  4. Serialization and Deserialization: Libraries like Gson or Jackson often need type tokens or wrapper classes because type erasure removes generic type details at runtime, making it harder to infer types during JSON parsing.
  5. Java Compiler Error Handling: Java generics type erasure helps in using the generic type constraints at compile time, and removing the type afterward to make sure that there is simpler bytecode and efficient JVM execution.
  6. Framework Design: Frameworks like Spring and Guice rely on type erasure to treat dependencies at runtime while using compile-time checks for correctness.

Conclusion

From this article, we conclude that Java removes the generic types at runtime and leaves only the raw types. This helps with compatibility but can cause issues like ClassCastException, heap pollution, and loss of type details. Since instanceof does not work with generics, extra care should be taken. To avoid these problems, developers should use bounded types, avoid raw types, and limit unchecked casts for safer code.

Learn Java Generics PECS and how to use it effectively in this blog.

Useful Resources:

Type Erasure in Java – FAQs

Q1. How does Java handle type erasure in Java generics?

The Java compiler applies type erasure by replacing all generic type parameters in your code with their first bound if the type parameter is bounded, or with Object if the type parameter is unbounded. This ensures that Java generics remain backward compatible and work with older, non-generic code.

Q2. Why are generics in Java checked at compile time if there is type erasure?

Even though type erasure in Java removes type information at runtime, the compiler performs type checking at compile time to ensure type safety. This prevents runtime errors caused by incorrect type assignments and reduces the need for explicit casting when using Java generics.

Q3. What is the difference between type erasure and reified generics in Java?

Type erasure in Java removes generic type information at runtime, meaning the JVM cannot directly access the parameterized type. In contrast, reified generics (as seen in languages like Kotlin) retain full type information at runtime, allowing type checks and reflective operations without workarounds such as type tokens.

Q4. What problem do generics in Java solve?

Generics in Java solve the problem of runtime type errors by enforcing type safety at compile time. Before generics, collections like List or ArrayList stored objects as raw types, which often led to ClassCastException at runtime. Generics provide a way to create reusable, type-safe, and maintainable code.

Q5. How do generics in Java improve type safety?

Generics ensure that a collection or method only works with the specified type, reducing runtime errors and eliminating the need for explicit type casting. For example, a List guarantees that only String objects can be added, leveraging compile-time type checking in Java generics.

Q6. What are raw types in Java?

Raw types in Java are generic classes or interfaces used without specifying a type parameter. Using raw types bypasses compile-time type checks and can lead to heap pollution, unchecked warnings, and runtime errors. Avoiding raw types is a best practice when working with Java generics and type erasure.

Q7. How can developers work around type erasure in Java?

Use type tokens, Class objects, or Java reflection to retain generic type information and safely handle generics at runtime despite type erasure.

Q8. Does type erasure affect performance in Java generics?

Java type erasure has minimal performance impact since type checks occur at compile time, and runtime code operates on raw types without extra overhead.

About the Author

Technical Research Analyst - Full Stack Development

Kislay is a Technical Research Analyst and Full Stack Developer with expertise in crafting Mobile applications from inception to deployment. Proficient in Android development, IOS development, HTML, CSS, JavaScript, React, Angular, MySQL, and MongoDB, he’s committed to enhancing user experiences through intuitive websites and advanced mobile applications.

Full Stack Developer Course Banner