What happens to generics when they get compiled? It turns out they are converted into casts. (Which may feel quite odd, as we used generics as a way of avoiding casting!)

Erasure is a fancy word for this process, from the idea that the generics themselves are erased.

Here are some examples below of how the types are erased:

// In Collections, the generic type is erased and leaves the rawtype.
List<String> -> List

// An unbounded generic is erased to the Object type
T without bounds -> Object

// An upper bounded wildcard is erased to the upper bound.
T extends Foo -> Foo

To give the correct type information in the compiled code, the compiler then inserts casts and/or bridge methods.

Cast example:

List<String> list = new ArrayList<String>();
list.add("Hi");
String x = list.get(0);

gets converted to

List list = new ArrayList();
list.add("Hi");
String x = (String) list.get(0);

An example of a Bridge Method is at the bottom of this post, since it’s not needed to understand the rest of the post.

Implications of erasure

Once your code gets compiled, your generics become collections of rawtypes and cleverly placed casts. The result is that some of your generics may be erased and not adequently represented, because there isn’t a way of representing what you want. Thus, your code may have less information about your types than you think.

If you add a generic that is useless, your compiler will raise an error. The compiler is actually trying to be helpful, letting you know that generic type you added doesn’t actually do anything.

The following are some common examples of this.

Overloading

If the generic type is the only difference in the method signature, then overloading doesn’t work. This is because at compile time, the compiler will remove the generic type and thus the difference between the two methods.

So the following two method declarations:

public void print(List<String> strings)
public void print(List<int> ints)

will both be erased to

public void print(List strings)
public void print(List ints)

and you end up with two identical method declarations.

instanceof

If you do:

obj instanceof InstanceOfExample<String>

You’ll get the compile error Illegal generic type for instanceof. If this was allowed, then all generic types would compile to

obj instanceof InstanceOfExample

and you would not be able to distinguish between them.

Exceptions

public class UncompilableException<T> extends Exception

will give you Generic class may not extend java.throwable.lang. Similarly to above, this is compiled to:

public class UncompilableException extends Exception

and thus the generic types cannot be distinguished from each other.

Static variables

This isn’t a product of Erasure, but this is another example where generics can’t be used:

public class MobileDevice<T> {
    private static T os;
}

MobileDevice<Smartphone> phone = new MobileDevice<>();
MobileDevice<Pager> pager = new MobileDevice<>();
MobileDevice<TabletPC> pc = new MobileDevice<>();

Here, os is all three of Smartphone, Pager, and TabletPC type, which is clearly nonsense.

Performance issues

All generics are erased to some sort of Object or Interface. This means you cannot use primitive type such as an int for generics. Instead you would have to use Integer. So, you cannot do:

Pair<int, char> p = new Pair<>(8, 'a');

It would have to be

Pair<Integer, Character> p = new Pair<>(8, 'a');

These Objects require more memory than the primitive types, and thus impact performance in our programs.

Side note - Bridge methods

These are used when we extend a generic class or interface. The example below has been shamelessly ripped from the javadocs website.

public class Node<T> {

    public T data;

    public Node(T data) {
        this.data = data;
    }

    public void setData(T data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class ChildNode extends Node<Integer> {
    public MyNode(Integer data) {
        super(data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

After type erasure it looks more like this:

public class Node {

    // T has been cast to Object
    public Object data;

    // ..and here
    public Node(Object data) {
        this.data = data;
    }

    // ..and here
    public void setData(Object data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class ChildNode extends Node {
    public MyNode(Integer data) {
        super(data);
    }

    // ALERT!: method signature differs from parent
    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

Due to the removal of the generic types, the ChildNode setData method signature does not match the parent class’s signature. Without further help from the compiler, the method would not be overloaded.

A bridge method is created by the compiler to get around this problem. It copies the same method signature from the parent class, and then casts the arguments.

class MyNode extends Node {

    // Bridge method generated by the compiler
    //
    public void setData(Object data) {
        setData((Integer) data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }

    // ...
}