When two interfaces have conflicting return types, why does one method become default?
Asked Answered
N

1

13

In Java 8, if I have two interfaces with different (but compatible) return types, reflection tells me that one of the two methods is a default method, even though I haven't actually declared the method as default or provided a method body.

For instance, take the following code snippet:

package com.company;
import java.lang.reflect.Method;

interface BarInterface {}
class Bar implements BarInterface {}

interface FooInterface {
    public BarInterface getBar();
}

interface FooInterface2 extends FooInterface {
    public Bar getBar();
}

class Foo implements FooInterface2 {
    public Bar getBar(){
        throw new UnsupportedOperationException();
    }
}

public class Main {
    public static void main(String[] args) {
        for(Method m : FooInterface2.class.getMethods()){
            System.out.println(m);
        }
    }
}

Java 1.8 produces the following output:

public abstract com.company.Bar com.company.FooInterface2.getBar()
public default com.company.BarInterface com.company.FooInterface2.getBar()

This seems odd, not only because both methods are present, but also because one of the methods has suddenly and inexplicably become a default method.

Running the same code in Java 7 yields something a little less unexpected, albeit still confusing, given that both methods have the same signature:

public abstract com.company.Bar com.company.FooInterface2.getBar()
public abstract com.company.BarInterface com.company.FooInterface.getBar()

Java definitely doesn't support multiple return types, so this result is still pretty strange.


The obvious next thought is: "Okay, so maybe this is a special behavior that only applies to interfaces, because these methods have no implementation."

Wrong.

class Foo2 implements FooInterface2 {
    public Bar getBar(){
        throw new UnsupportedOperationException();
    }
}

public class Main {
    public static void main(String[] args) {
        for(Method m : Foo2.class.getMethods()){
            System.out.println(m);
        }
    }
}

yields

public com.company.Bar com.company.Foo2.getBar()
public com.company.BarInterface com.company.Foo2.getBar()

What's going on here? Why is Java enumerating these as separate methods, and how has one of the interface methods managed to become a default method with no implementation?

Necessaries answered 23/11, 2015 at 21:18 Comment(0)
R
9

It's not a default method you provide but a bridging method. In the parent interface you have defined.

public BarInterface getBar();

and you must have a method which can be called which implements this.

e.g.

FooInterface fi = new Foo();
BarInterface bi = fi.getBar(); // calls BarInterface getBar()

However, you also need to be able to call it's co-variant return type.

FooInterface2 fi = new Foo();
Bar bar = fi.getBar(); // calls Bar getBar()

These are the same method, only difference is that one calls the other and cast the return value. It's the method which appears to have a default implementation as it is on the interface which does this.

Note: if you have multiple levels of interfaces/class and each has a different return type, the number of methods accumulates.

The reason it does this is that the JVM allows having multiple methods with different return type because the return type is part of the signature. I'e the caller has to state which return type it is expecting and the JVM doesn't actually understand co-variant return types.

Rusticus answered 23/11, 2015 at 21:23 Comment(10)
I guess I'm still a little confused. Did the compiler build a default method for me that acts as a pass-through? Also: Given that Bar can be safely cast to BarInterface, why not just provide one method with that return type?Necessaries
@Necessaries I have updated my answer. The javac adds methods to get the JVM to do what Java requires without actually changing the JVM between versions. e.g. accessing private members in other classes also does this. Also for the JVM, the return type is part of the signature. Most recently under Oracle there has been some willingness to change the JVM, but not much.Rusticus
Umm.. but why that bridge method is required to be generated. Any method returning Bar can be assigned to BarInterface reference type. Also, I thought return type is not a part of method signature. Did you mean something else there?Threatt
@RohitJain Java != JVM. The JVM includes the return type in the method signature and it has to be an exact match. ie it does a String comparison.Rusticus
@PeterLawrey Oh.. Didn't know this thing. Does that mean for any covariant return type method overriding, a bridge method is generated by JVM?Threatt
@RohitJain yes. Co-variant return types were added after the JVM was first developed and the byte code requirements haven't changed much since.Rusticus
Ah. That makes sense -- Java doesn't consider the return type to be part of the signature (§8.4.2. of the language spec), but the JVM does consider this.Necessaries
@PeterLawrey Ah! Thank you so much. Never thought about this :)Threatt
@Necessaries Java cares about the generic type of the method, the JVM doesn't. There was a bug in Java 6 where you could use different generics in the method to have incompatible return types. vanillajava.blogspot.ro/2011/02/… After I pointed out this "cool" feature to the engineers they fixed it :(Rusticus
Since the question is about a difference between Java 7 and Java 8, it should be mentioned, that the Java 8 method is indeed a default method in the sense that it is not abstract but it bears the same semantic as in Java 7: before Java 8, FooInterface2 had two abstract methods on the bytecode level and the compiler generated a bridge method in the implementing class. Starting with Java 8, FooInterface2 defines a non-abstract bridge method (and non-abstract is equivalent to default) and the implementing class(es) don’t need to do so.Sag

© 2022 - 2024 — McMap. All rights reserved.