Type Erasure in Java Explained

Examples of Type Erasure

Posted by Mr.Humorous 🥘 on March 14, 2019

1. Overview

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

2. What is Type Erasure?

Type erasure can be explained as the process of enforcing type constraints only at compile time and discarding the element type information at runtime.

For example:

public static  <E> boolean containsElement(E [] elements, E element){
    for (E e : elements){
        if(e.equals(element)){
            return true;
        }
    }
    return false;
}

When compiled, the unbound type E gets replaced with an actual type of Object:

public static  boolean containsElement(Object [] elements, Object element){
    for (Object e : elements){
        if(e.equals(element)){
            return true;
        }
    }
    return false;
}

The compiler ensures type safety of our code and prevents runtime errors.

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 E 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 integerStack is a Stack<Integer> type.

So, not surprisingly, an attempt to pop a String and assign to an Integer causes a ClassCastException from 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.

5. Conclusion

In this tutorial, we’ve discussed the concept of type erasure with examples in type parameter variables and methods.

You can read more about these concepts:

As always, the source code that accompanies this article is available over on GitHub.