Generics were introduced to implement generic programming and control type-safety at compile-time. The feature has been available since Java 5. To support backward compatibility with previous Java versions, information about generic types is erased by the compiler. We will cover exactly which types are erased in future topics.

This means that at runtime all these objects will have the same type:

// Generic types
List<Integer> integers = new List<>();
List<String> strings = new List<>();

// Raw type
List objects = new List();

The transformation process is called type erasure. Let’s take a closer look at what it actually does.

Generics replacement

First of all, type erasure replaces parameters of generic types with their bounds. Unbounded types are replaced by Object. This means that information about types is erased when a program is translated into byte code. As a result, byte code contains only ordinary non-generic classes and interfaces.

Let’s consider the generic class Data:

class Data<T> {
    private T data;

    public T get() {
        return data;
    }

    public void set(T data) {
        this.data = data;
    }
}

The Java compiler will replace the parameter T with Object, because T is unbounded. Below is code that is effectively the same as Data<T> after compilation:

class Data {
    private Object data;

    public Object get() {
        return data;
    }

    public void set(Object data) {
        this.data = data;
    }
}

Now suppose Data is parameterized by <T extends Number>. In this case, the transformed code will look similar to the last code snippet with one difference: Object will be replaced by Number.

If a value is assigned, a generic replacement can affect the accuracy of the program. If it is necessary to preserve type safety, the compiler inserts type casting. Let’s look at the code:

Data<String> data = new Data("stored value");
String stored = data.get();

After type erasure is performed the code above is equivalent to:

Data data = new Data("stored value");
String stored = (String) data.get();

Bridge methods

In order to preserve polymorphism through type casting, sometimes the compiler has to generate synthetic methods. Let’s consider an extension of the Data class:

public class NumberData extends Data<Number> {
    public void set(Number number) {
        System.out.println("NumberData set");
        super.set(number);
    }
}

After type erasure, the NumberData method remains set(Number number), while the original Data method is set(Object obj). Because NumberData extends Data, it is possible to invoke set(Object obj) from an instance of NumberData and set objects of arbitrary type. But we only want to set objects of the Number type. To solve this problem and preserve the polymorphism of generic types after type erasure, the Java compiler generates a so-called bridge method in the NumberData class. It overrides parameterized parent methods and provides type casting to specific parameters:

public class NumberData extends Data {
    // Bridge method generated by the compiler
    public void set(Object object) {
        set((Number) object);
    }

    public void set(Number number) {
        super.set(number);
    }
    ...
}

A bridge method is a synthetic method created by the compiler as part of the type erasure process. It exists in byte code only and not available for direct usage from Java code. Normally you don’t directly encounter bridge methods, although sometimes they might appear in a stack trace.

Conclusion

Type erasure is an operation that the Java Virtual Machine (JVM) runs during the compilation of source code to byte code. It replaces generic type parameters with their upper bounds. It also inserts type casting and generates bridge methods to preserve type safety. Type erasure plays an important role in Java’s implementation of generics.

Leave a Reply

Your email address will not be published.