Advance Java

Learn New things from Rixosys

What Is Type Erasure?

1. Overview

In this quick article, we’ll discuss the basics of an important mechanism in Java’s generics known as type erasure.

2.Type Erasure in Java


Generics concept is introduced in Java language to provide tighter type checks at compile time and to support generic programming. The way to implement 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.

In general, the compiled generic code actually just uses java.lang.Object wherever you talk about T (or some other type parameter) – and there’s some metadata to tell the compiler that it really is a generic type. When you compile some code against a generic type or method, the compiler works out what you really mean (i.e. what the type argument for T is) and verifies at compile time that you’re doing the right thing, but the emitted code again just talks in terms of java.lang.Object – the compiler generates extra casts where necessary. At execution time, a List and a List are exactly the same; the extra type information has been erased by the compiler.

How Type Erasure works according to the Program?

// Here, T is bounded by Object i.e. java.lang.Object 
class GFG<T> { 
// Here, T will be replaced by default i.e. Object 
    T obj;  
  
    GFG(T o) 
    { 
        obj = o; 
    } 
    T getob() 
    { 
        return obj; 
    } 
} 

After compilation, the code is replaced by default Object like the below:

class GFG
{
// Here, T will be replaced by default i.e. Object
    Object obj;
    GFG(Object o)
    {
        obj=o;
    }
    Object getob()
    {
        return obj;
    }
}

After compilation, the code is replaced by String like the below:

class Geeks
{

//Here, T will be replaced by String i.e. java.lang.String
    String str;

    Geeks(String o)
    {
        str=o;
    }
    String getob()
    {
        return str;
    }
}

NOTE: After the compilation of above two classes, we can check the contents of both the classes. After the compilation, in GFG class T will be replaced by Object and in Geeks class T will be replaced by String. We can check the content by running the compiled code by javap className command.

3. Types of Type Erasure

Type erasure can occur at class (or variable) and method levels.

3.1. Class Type Erasure

At the class level, type parameters on the class are discarded during code compilation and replaced with its first bound, or Object if the type parameter is unbound.

Let’s implement a Stack using an array:

public class Stack<E> {
    private E[] stackContent;
 
    public Stack(int capacity) {
        this.stackContent = (E[]) new Object[capacity];
    }
 
    public void push(E data) {
        // ..
    }
 
    public E pop() {
        // ..
    }
}
Upon compilation, the unbound type parameter E is replaced with Object:
public class Stack {
    private Object[] stackContent;
    public Stack(int capacity) {
        this.stackContent = (Object[]) new Object[capacity];
    }
    public void push(Object data) {

        // ..
    }
    public Object pop() 
    {
        // ..
    }
}
In a case where the type parameter E is bound:
public class BoundStack<E extends Comparable<E>> 
{
    private E[] stackContent;
    public BoundStack(int capacity) {
        this.stackContent = (E[]) new Object[capacity];
    }
    public void push(E data) {
        // ..
    }
    public E pop() {
        // ..
    }
}
When compiled, the bound type parameter E is replaced with the first bound class, Comparable in this case:
public class BoundStack {
    private Comparable [] stackContent;
    public BoundStack(int capacity) {
        this.stackContent = (Comparable[]) new Object[capacity];
    }
    public void push(Comparable data) {
        // ..
    }
    public Comparable pop() {
        // ..
    }
}
3.2. Method Type Erasure

For method-level type erasure, the method’s type parameter is not stored but rather converted to its parent type Object if it’s unbound or it’s first bound class when it’s bound.

Let’s consider a method to display the contents of any given array:

public static <E> void printArray(E[] array) {
    for (E element : array) {
        System.out.printf("%s ", element);
    }
}
Upon compilation, the type parameter E is replaced with Object:
public static void printArray(Object[] array) {
    for (Object element : array) {
        System.out.printf("%s ", element);
    }
}

For a bound method type parameter:

public static <E extends Comparable<E>> void printArray(E[] array) {
    for (E element : array) {
        System.out.printf("%s ", element);
    }
}

We’ll have the type parameter erased and replaced with Comparable:

public static void printArray(Comparable[] array) {
    for (Comparable element : array) {
        System.out.printf("%s ", element);
    }
}

4. Edge Cases

Sometime during the type erasure process, the compiler creates a synthetic method to differentiate similar methods. These may come from method signatures extending the same first bound class.

Let’s create a new class that extends our previous implementation of Stack:

public class IntegerStack extends Stack<Integer> {
    public IntegerStack(int capacity) {
        super(capacity);
    }
    public void push(Integer value) {
        super.push(value);
    }
}

Now let’s look at the following code:

              IntegerStack integerStack = new IntegerStack(5);

              Stack stack = integerStack;

              stack.push("Hello");

              Integer data = integerStack.pop();

After type erasure, we have:

                 IntegerStack integerStack = new IntegerStack(5);

                 Stack stack = (IntegerStack) integerStack;

                 stack.push("Hello");

                 Integer data = (String) integerStack.pop();

Notice how we can push a String on the IntegerStack – because IntegerStack inherited push(Object)from the parent class Stack. This is, of course, incorrect – as it should be an integer since integerStackis a Stack<Integer> type.

So, not surprisingly, an attempt to pop a String and assign to an Integer causes a ClassCastExceptionfrom a cast inserted during the push by the compiler.

4.1. Bridge Methods

To solve the edge case above, the compiler sometimes creates a bridge method. This is a synthetic method created by the Java compiler while compiling a class or interface that extends a parameterized class or implements a parameterized interface where method signatures may be slightly different or ambiguous.

In our example above, the Java compiler preserves polymorphism of generic types after erasure by ensuring no method signature mismatch between IntegerStack‘s push(Integer) method and Stack‘s push(Object) method.

Hence, the compiler creates a bridge method here:

public class IntegerStack extends Stack {
    // Bridge method generated by the compiler
     
    public void push(Object value) {
        push((Integer)value);
    }
    public void push(Integer value) {
        super.push(value);
    }
}

Consequently, Stack class’s push method after type erasure, delegates to the original push method of IntegerStack class.