This post was initially on the Ed forums for CS2030S.

I first created this write-up after a consultation with the professor on this topic of bridge methods. In the end, it was on the final exam for 10% of the total marks.

Motivation

Consider the following piece of code:

class Main {
    public static void main(String[] args) {
        A<String> a = new B();
        a.foo("");
    }
}

class A<T> {
    void foo(T t) {
        System.out.println("T");
    }
}

class B extends A<String> {
    @Override
    void foo(String str) {
        System.out.println("String");
    }
}

The code outputs String as the output. This is to be expected when we substitute String into T. Then, since the method signatures for A::foo(String) and B::foo(String) are the same, we can use the @Override annotation on B::foo(String) without error.

At compile time, the dynamic binding process first finds the most specific method (only one choice here) for foo in A, which is foo(String). At runtime, the method in the run-time type of the target is invoked, that is, B::foo(String).

Using generics means there will be type erasure at compile-time. What would our code look like then? Suppose that bridge methods don’t exist.

Code after type erasure (no bridge methods):

class Main {
    public static void main(String[] args) {
        A a = new B();
        a.foo("");
    }
}

class A {
    void foo(Object t) {
        System.out.println("T");
    }
}

class B extends A {
    void foo(String s){
        System.out.println("String");
    }
}

The code with generics and the code with type erasure call different methods (B::foo(String) and A::foo(Object) respectively) and have different outputs. How do we make the two outputs the same? Enter bridge methods.

Bridge methods

The solution that Java presents is to add extra code during type erasure. Since it is a method added by the compiler (and not us), it is called a synthetic method. A bridge method is specifically “a synthetic method that the compiler generates in the course of type erasure” [2].

Using the example of the code above, the actual code after type erasure looks something like:

class Main {
    public static void main(String[] args) {
        A a = new B();
        a.foo("");
    }
}

class A {
    void foo(Object t) {
        System.out.println("T");
    }
}

class B extends A{
    void foo(String s) {
        System.out.println("String");
    }
    
    void foo(Object o){
        //we shouldn't need instanceof check
        //because compiler generates after
        //checking the generics code
        this.foo((String) o);
    }
}

At compile time, the dynamic binding process first finds the most specific method (only one choice here) for foo in A, which is foo(Object). At runtime, the method in the run-time type of the target is invoked, that is, B::foo(Object). Within B::foo(Object), we see that all it really does is to forward the invocation to B::foo(String).

The methods called and outputs of the generic code and the type erasure code now match.

Bridge methods are generated “when a type extends or implements a parameterized class or interface and type erasure changes the signature of any inherited method.” [2]

Crucially, methods do not have to be overridden for a bridge method to be generated for it. It just has to have a different signature after type erasure.

Creating errors

In [1], they give an example of something that could go wrong. In the context of A and B:

class Main {
    public static void main(String[] args) {
        A a = new B(); //this code uses raw types
        a.foo(1);
    }
}

class A<T> {
    void foo(T t) {
        System.out.println("T");
    }
}

class B extends A<String> {
    @Override
    void foo(String s){
        System.out.println("String");
    }
}

Running the code above results in a ClassCastException. Consider the type erasure equivalent:

class Main {
    public static void main(String[] args) {
        A a = new B(); 
        a.foo(1);
    }
}

class A {
    void foo(Object t) {
        System.out.println("T");
    }
}

class B extends A{
    void foo(String s) {
        System.out.println("String");
    }
    
    void foo(Object o){
        //we shouldn't need instanceof check
        //because compiler generates after
        //checking the generics code
        this.foo((String) o);
    }
}

The same runtime error occurs. Without bridge methods, there would have been no error. Our two codes are consistent in being wrong, so there is no unexpected behaviour. The compiler casts without checking in the type erasure code to mimic the generic code in error.

Another type of error that could occur is that the bridge method cannot be generated:

class Main {
    public static void main(String[] args) {
        A<String> a = new B();
        a.foo("");
    }
}

class A<T> {
    void foo(T t) {
        System.out.println("T");
    }
}

class B extends A<String> {
    void foo(Object o) {
        //this will not compile
    }
}

This is a compile error. Two methods cannot have the same signature without overriding the other. However, this is exactly what would happen in our type erasure code with a potential bridge method and B::foo(Object). Hence, the compilation will fail. The code would have looked like:

class Main {
    public static void main(String[] args) {
        A a = new B();
        a.foo("");
    }
}

class A {
    void foo(Object t) {
        System.out.println("T");
    }
}

class B extends A {
    void foo(Object o) {
        //from original code
    }
    void foo(Object o) {
        //bridge method
    }
}

Both the generic code and the type erasure code must both compile without error for the program to be run. Since the type erasure code is generated from the generic code, if Java cannot generate the type erasure code, then it will refuse to compile the generic code.

References

[1] Java Tutorial on Bridge Methods

[2] “What is a bridge method?” and “Under what circumstances is a bridge method generated?”