Search code examples
javagenericsoverridingbridge

Why Java compiler creates bridge methods for methods without parameters (getter methods)


I do understand the need to create bridge methods for example a setter method that expects an argument to be passed, but what about a getter method? Why does Java produce a bridge method as well?

Here is a dummy code produced by ChatGPT to make things more specific:

class Box<T> {
private T value;

public void setValue(T value) {
    this.value = value;
}

public T getValue() {
    return value;
}
}

class StringBox extends Box<String> {
public void setValue(String value) {
    System.out.println("Setting a String value in StringBox");
    super.setValue(value.toString());
}

public String getValue() {
    System.out.println("Getting String value from StringBox");
    return super.getValue();
}
}

The Box class after the erasure will have the following method: public Object getValue(); In the StringBox class, we defined the method: public String getValue() But these two methods have the same signature and the return type of the overriding method uses String as return type, which is a subclass of Object class(Covariant Return Type) So it seems that overriding is already complete. Why is there a need for the bridge method? What am i missing?


Solution

  • Actually, the JVM doesn't implement return type covariance. Even without generics, Java requires bridge methods to implement return type covariance on the JVM. If you compile the following,

    class MySupplier {
        public Object supply() { return null; }
    }
    class HelloSupplier extends MySupplier {
        @Override public String supply() { return "Hello, World!"; }
    }
    

    you will find that HelloSupplier contains bytecode amounting to the following:

    class HelloSupplier extends MySupplier {
        public String supply() { return "Hello, World!"; }
        public /*bridge*/ Object supply() { return this.supply()/*String*/; }
        // in bytecode, all method calls include the signature of the method, including the return type (which is written after the method name and parameters)
        // the bridge method supply()Object calls the method supply()String
        // note that bytecode allows overloading on return type, even though Java doesn't
    }
    

    If you look at the JVM definition of an override, you will notice that it differs from Java's idea of an override, in that the signatures of overrides must match exactly in the eyes of the JVM. You don't get to relax the parameter types or strengthen the return type.

    JVM Specification SE 20, Section 5.4.5

    An instance method mC can override another instance method mA iff all of the following are true:

    • mC has the same name and descriptor as mA.
    • ...

    This is important, because this is how the JVM decides what method to call when you do a virtual lookup. A virtual call (invokevirtual) to MySupplier#supply()Object on a HelloSupplier object will never call HelloSupplier#supply()String, since a supply()String method does not override a supply()Object method in the eyes of the JVM (even though it does in Java). Such a virtual method call will call HelloSupplier#supply()Object, if it exists, or else look for such a method in superclasses. The Java compiler, in order to implement return type covariance, must override MySupplier#supply()Object in the only way the JVM understands: by defining HelloSupplier#supply()Object.

    Because the JVM doesn't implement return type covariance anyway, the Java compiler can't leverage return type covariance to sometimes avoid emitting bridges for generic code.