Type erasure

Type erasure in Java #

Java compilers rely on an approach called type erasure to compile:

  • type parameters (e.g. Box<Integer>),
  • type variables (e.g. Box<T>), and
  • question marks (e.g. Box<? extends Number>).

More often than not, when a Java program does not compile, we can understand why without understanding the compilation procedure. However, for generics, some basic understanding of type erasure can help.

Procedure #

Intuitively, type erasure consists in producing a program equivalent to the initial one, but free of generic or parameterized types (i.e. intuitively with no “diamond”).

Informally, a Java compiler first performs all type checks specified in the program. If all type checks succeed, then:

  • each type variable or question mark is replaced with its (upper or lower) bound (if any), or Object if there is none,
  • explicit casts are introduced where they become necessary (as well as so-called bridge methods if needed, to ensure that overriden methods remain overriden),
  • all type parameters, type variables and question marks are deleted.

Illustrations #

Warning. Type erasure is traditionally illustrated with a Java program before and after erasure (for instance here, but also in Oracle’s Java tutorials). However, these illustrations should not be taken literally, because compilation produces byte code, not source code.

Example. Consider the generic class Box<T> that we used earlier to illustrate covariance and contravariance, together with the methods unbox and replaceValue:

public class Box<T> {

    T boxedValue;

    public Box(T boxedValue){
        this.boxedValue = boxedValue;
    }

    T getValue(){
          return boxedValue;
    }

    void setValue(T newValue){
          boxedValue = value;
    }
}

$\qquad$

Number unbox(Box<? extends Number> box){
    return box.getValue();
}

void replaceValue(Box<? super Integer> box, Integer integer){
    box.setValue(integer);
}

The bytecode generated after type erasure may be equivalent to bytecode generated for the following program:

public class Box {

    Object boxedValue;

    public Box(Object boxedValue){
        this.boxedValue = boxedValue;
    }

    Object getValue(){
          return boxedValue;
    }

    void setValue(Object newValue){
          boxedValue = value;
    }
}

$\qquad$

Number unbox(Box box){
    return (Number) box.getValue();
}

void replaceValue(Box box, Integer integer){
    box.setValue(integer);
}

Example. Generic methods are compiled in a similar way.

For instance, consider once again our generic method

<T extends Unit> T healthiest(T[] units) {
    ...
}

The bytecode generated after type erasure may be equivalent to bytecode generated for a method:

Unit healthiest(Unit[] units) {
    ...
}

Consequences #

Runtime #

A first important consequence of type erasure (e.g. for debugging) is that parameterized types do not exist at runtime (as opposed C++ or C#).

For instance, a JVM cannot distinguish an instance of Box<Integer> from an instance of Box<String>. Both are instances of the same class Box.

Compile-time #

Type erasure may also explain in some cases why a Java program are invalid.

We list here two simple cases (among others).

Ambiguity #

Java (like C++ or C#) allows defining two methods with the same name in the same class, as long as they have different argument types (this is called method overloading). However, after type erasure, this may result in two methods with the same name and arguments (which is forbidden).

Example. The following program does not compile:

public class MyClass<T,V> {

    void myMethod(String s, T t) {
        ...
    }

    void myMethod(String w, V v) {
        ...
    }
}
Constructor #

A type parameter cannot be instantiated.

Example. The following program does not compile:

public class Box<T> {

    T create(){
        return new T();
    }
}