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 Classes
- Generic Methods
What Is Type Erasure in Java and How It Works
Type erasure in Java means checking the type constraints at compile time and removing the type information at 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 type checks at compile time and to support 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 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.
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
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, 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 the 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.
What are Bridge Methods in Java and Why Do They Matter
Bridge methods are automatically generated by the Java compiler to maintain the 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
}
}
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.
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 makes sure that 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
}
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
What Is 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:
Output:
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.
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 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. Also, the concept of raw types in Java shows the outcome of type erasure in Java; generic classes lose type parameters after compilation.
Example:
Output:
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.
Get 100% Hike!
Master Most in Demand Skills Now!
Type Erasure vs Type Inference in Java
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 |
Differences Between Type Erasure and Reified Generics
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 |
Best Practices for Using Type Erasure in Java
- 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:
- 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.
- 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.
- Java Reflection: Because of type erasure in Java generics, 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.
- 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.
- Java Compiler Error Handling: Type erasure in Java generics 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.
- 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.
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 Java 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 in Java?
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 in Java?
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.
Q6. What is raw types in java?
Raw types in Java are generic types which are used without any type arguments.