Type Erasure in Java

When the Java code is compiled, before generating the bytecode, the Java compiler performs type erasure on the 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 the runtime.

In this blog, we will discuss the Java generics type erasure in detail.

Table of Contents:

What are Java Generics?

Generics in Java allow creating the classes, interfaces, and methods with the type parameters, and ensures the type safety and reduce the need for the type casting. 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 of 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 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:

  1. Generic Classes
  2. Generic Methods

What Is Type Erasure?

Type erasure in Java means checking the type constraints at the compile time and removing the type information at the runtime. 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 the type checks at compile time and to support the generic programming. When using generics, the Java compiler applies type erasure to:

  • 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.
  • Generate bridge methods to preserve polymorphism in extended generic types.

Type erasure 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.

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. Type Parameter Erasure (Class Level Erasure)

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 the Object while compiling. This enables the code to be used by older Java versions that are not compatible with the generics. All methods are referencing T as Object after the type erasure, so there is only type safety at the compile time. When the values are extracted, explicit casting 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, all the methods and the fields using T now can use the Number, which keeps the type safety for the numeric values.

2. Method Type Erasure

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 the Object during the type erasure. This makes the method work with any data type and also keeps it compatible with the 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. Type Erasure in 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 casting 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.

Bridge Methods in Java

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

When a subclass overrides a method from a generic superclass, type erasure 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
    }
}

How Does Type Erasure Affect Generics Code?

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

1. Type Safety

Type erasure ensures the 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 the runtime, and if incorrect casting 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 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 makes sure that compatibility with the 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.

Example:

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

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

Limitations of Type Erasure

Although type erasure provides compatibility, it introduces some significant 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

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.

Non-Reifiable type

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 the runtime due to Java’s type erasure. 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.

Example:

Java

Output:

Non-Reifiable type

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

Get 100% Hike!

Master Most in Demand Skills Now!

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 the 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.

To know more about this topic you can refer to our Java Course.

Type Erasure in Java – FAQs

Q1. How does Java handle type erasure in generics?

The Java compiler erases all the type parameters and replaces each of them with its first bound if the type parameter is bounded or Object if the type parameter is unbounded.

Q2. Why are generics checking if there is type erasure?

To provide tighter type checks at the compile time and to support the generic programming.

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

Reified generics have support in the compiler for storing the type information, whereas the type erased generics don’t.

Q4. What problem do generics solve?

Aside from performance, however, there’s no way to determine the type of data in the list at compile time, which makes for some fragile code. Generics solve this problem.

Q5. How do generics improve type safety?

They catch the type errors at the compile time. Hence, they improve the type security.

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