Java Generics And Type Erasure

30 May 2020

Strongly typed programming languages like Java, C++, C#, etc. has great benefits because of its typed nature. Forcing the users to define the type of entity upfront allows the programming language to provide type safety guarantees. Strong typing has its own advantages, and a whole blog post can be written on it. Some of the essential gains are early-type error detection, compile-time optimization, documentation, etc. But this advantage can also be constraining in writing generic, type agnostic, reusable code if not supported correctly. That is precisely why these languages usually allow writing generically typed entities. For example, in Java it’s called generics, in C++ it’s called templates and parametric polymorphism in Haskell and so on. This style of programming is called Generic Programming. In this style of programming, programs are written with types that are to be defined later by providing type as a parameter when initiated. Every language has it’s own way of implementing and supporting generic types. In this blog post, we will look at how Java does it under the hood.

During compilation, Java compiler replaces erasure type with type Object if the generic type is unbounded. For example:

public class Test<T> {

    private final T a;

    public Test(T a) {

        this.a = a;

    }

    public T getA() {

        return a;

    }

}

converts into this after compilation:

public class Test {

 private final Object a;

    public Test(Object a) {

        this.a = a;

    }

    public Object getA() {

        return a;

    }
}

This is called type erasure. After compilation, JVM has no knowledge about the actual type of the generic type. That’s why you might see some type mismatch errors while dealing with generic type classes. Take this as an example:

public class Test<T> {
    private T a;
    public Test(T a) {
        this.a = a;
    }

    public T getA() {
        return a;
    }

    public void setA(T a) {
        this.a = a;
    }
}

public class MyTest extends Test<Integer> {
    public MyTest(Integer a) {
        super(a);
    }

    public void setA(Integer a) {
        super.setA(a);
    }
}

public class Main {

    public static void main(String[] args) {
        Test t = new MyTest(10);
        int a = t.getA() + 10; //Error: Operator '+' cannot be applied to 'java.lang.Object','int'
    }
}

In the code above, we get a type mismatch error because the compiler thinks the type of field a inside Test class is Object as a result of type erasure. The way to fix this is by typecasting t.getA() with Integer. So the code becomes this

public class Main {

    public static void main(String[] args) {
        Test t = new MyTest(10);
        int a = (Integer)t.getA() + 10; //No error
    }
}

and we don’t see any errors.

Type erasure can also lead to some interesting problems. For example, after type erasure the code above turns into this:

public class Test {
    private Object a;
    public Test(T a) {
        this.a = a;
    }

    public Object getA() {
        return a;
    }

    public void setA(T a) {
        this.a = a;
    }
}

public class MyTest extends Test {
    public MyTest(Integer a) {
        super(a);
    }

    public void setA(Integer a) {
        super.setA(a);
    }
}

As you can see, class myTest is no longer overriding the setA method that it intended to. To maintain the polymorphism of the generic types, the compiler introduces something called a Bridge Method. It works like this:

public class Test {
    private Object a;
    public Test(T a) {
        this.a = a;
    }

    public Object getA() {
        return a;
    }

    public void setA(T a) {
        this.a = a;
    }
}

public class MyTest extends Test {
    public MyTest(Integer a) {
        super(a);
    }

        **// Bridge method generated by the compiler
        public void setA(Object a) {
            setA((Integer)a);
        }**

    public void setA(Integer a) {
        super.setA(a);
    }
}

You can verify the existence of this bridge method by printing the type of parameters using java reflection.

Here is the code:

import java.lang.reflect.Method;

public class Main {

    public static void main(String[] args) {
        try {
            // create class object
            Class classobj = MyTest.class;

            // get list of methods
            Method[] methods = classobj.getMethods();

            // get the name of every method present in the list
            for (Method method : methods) {

                String methodName = method.getName();
                if (methodName.equals("setA")) {
                    System.out.println("Class " + classobj.getName() +" Contains"
                            + " Method whose name is "
                            + methodName);
                    System.out.println("This method has parameters whose types are:");
                    for(Class c : method.getParameterTypes()) {
                        System.out.println(c.getName());
                    }
                    System.out.println();
                }
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

The output looks like this:

Class MyTest Contains Method whose name is setA
This method has parameters whose types are:
java.lang.Integer

Class MyTest Contains Method whose name is setA
This method has parameters whose types are:
java.lang.Object

So that’s how Java handles generic types under the hood. There are some other restrictions on Java generics that you can learn more about from here.

All the code in this post is available on my github profile.

Discussion, links, and tweets

Software Engineer at AWS using simulation to train robots. I am obsessed about history.