Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Eclipse/javac disagree on compiling signature with default method collision; who is right?

Here's a simple class that demonstrates the issue:

package com.mimvista.debug;

public class DefaultCollisionTest {
    public static interface Interface1 {
        public String getName();
    }

    public static interface Interface2 {
        public default String getName() { return "Mr. 2"; };
    }

    public static <X extends Interface1&Interface2> String extractName(X target) {
        return target.getName();
    }
}

Eclipse (Neon 2) happily compiles this class while javac (JDK 1.8.0_121) spits out the following compile error:

$ javac src/com/mimvista/debug/DefaultCollisionTest.java
src\com\mimvista\debug\DefaultCollisionTest.java:13: error: class INT#1 inherits abstract and default for getName() from types Interface2 and Interface1
        public static <X extends Interface1&Interface2> String extractName(X target) {
                       ^
  where INT#1 is an intersection type:
    INT#1 extends Object,Interface1,Interface2
1 error

I believe that Eclipse is correct in this case but I'm not totally sure. Based on my understanding of the "inherits abstract and default" error, I think it should only be generated when compiling an actual declared class that implements those two interfaces. It seems like javac may be generating an intermediate class under-the-hood to deal with that generic signature and erroneously subjecting it to the default method collision test?

like image 225
BonusLord Avatar asked Aug 21 '17 13:08

BonusLord


3 Answers

Javac is correct according to JLS 9.4.1.3. Interfaces > Inheriting Methods with Override-Equivalent Signatures:

If an interface I inherits a default method whose signature is override-equivalent with another method inherited by I, then a compile-time error occurs. (This is the case whether the other method is abstract or default.)

The small print explains:

[...] when an abstract and a default method with matching signatures are inherited, we produce an error. In this case, it would be possible to give priority to one or the other - perhaps we would assume that the default method provides a reasonable implementation for the abstract method, too. But this is risky, since other than the coincidental name and signature, we have no reason to believe that the default method behaves consistently with the abstract method's contract - the default method may not have even existed when the subinterface was originally developed. It is safer in this situation to ask the user to actively assert that the default implementation is appropriate (via an overriding declaration).

In contrast, the longstanding behavior for inherited concrete methods in classes is that they override abstract methods declared in interfaces (see §8.4.8). The same argument about potential contract violation applies here, but in this case there is an inherent imbalance between classes and interfaces. We prefer, in order to preserve the independent nature of class hierarchies, to minimize class-interface clashes by simply giving priority to concrete methods.

Also compare with 8.4.8.4. Classes > Inheriting Methods with Override-Equivalent Signatures:

It is a compile-time error if a class C inherits a default method whose signature is override-equivalent with another method inherited by C, unless there exists an abstract method declared in a superclass of C and inherited by C that is override-equivalent with the two methods.

This exception to the strict default-abstract and default-default conflict rules is made when an abstract method is declared in a superclass: the assertion of abstract-ness coming from the superclass hierarchy essentially trumps the default method, making the default method act as if it were abstract. However, the abstract method from a class does not override the default method(s), because interfaces are still allowed to refine the signature of the abstract method coming from the class hierarchy.

In even plainer words: the assumption is that the two interfaces are logically unrelated and both specify some kind of a behaviour contract. Therefore it's not safe to assume that the default implementation in Interface2 is a valid fulfilment of the contract of Interface1. It's safer to throw an error and let the developer sort it out.

I didn't find a place in the JLS where it would exactly tackle your case, but I think the error is in the gist of the above specifications - you declare that extractName() should take an object that implements both Interface1 and Interface2. But for such an object it would only be valid if "there exists an abstract method declared in a superclass of C and inherited by C that is override-equivalent with the two methods". Your generic declaration does not specify anything about the superclass of X, so the compiler treats it as a "abstract-default" clash.

like image 173
Adam Michalik Avatar answered Oct 30 '22 18:10

Adam Michalik


Eclipse is right.

I have not found this javac bug in the Java Bug Database and therefore reported it: JDK-8186643

Better explanation by Stephan Herrmann (see his comment below):

Right, reporting an error against an intersection type should only happen when the intersection type is not well-formed and hence the intersection is empty. But as this answer shows, the intersection is not empty and should thus be legal. Actually, the error message class INT#1 inherits ... makes no sense, because at that point nobody mentioned a class INT#1, we only have the intersection of two interfaces, and that intersection is used only as a bound, not as a type.

A class that implements multiple interfaces of the same method can be compiled with both compilers, even if the method of one interface has a default implementation. The class can be referenced as <T extends I1 & I2> as long as neither I1 nor I2 has a default implementation for a equally named method. Only if one of the two interfaces has a default implementation javac fails.

In case of ambiguity which implementation should apply, the error should already occur when defining a class, not when the class is referred as <T extends ...> (see JLS 4.9. Intersection Types).

See following example which works with <T extends I1 & I2> and <T extends IDefault>, but fails with <T extends I1 & IDefault> and javac:

interface I1 {
    String get();
}

interface I2 {
    String get();
}

interface IDefault {
    default String get() {
        return "default";
    };
}

public class Foo implements I1, I2, IDefault {

    @Override
    public String get() {
        return "foo";
    }

    public static void main(String[] args) {
        System.out.print(getOf(new Foo()));
    }

//  static <T extends I1 & IDefault> String getOf(T t) { // fails with javac
    static <T extends I1 & I2> String getOf(T t) { // OK
        return t.get();
    }

}
like image 6
howlger Avatar answered Oct 30 '22 18:10

howlger


As I understand it, the question is about passing an object of an already compiled class as a parameter. Since you cannot call the extractName(X) method with an abstract class or an interface, the argument object must have it's getName() method resolved and unambiguous. Java uses a late binding for resolving which overridden method is called at runtime, so I would agree with BonusLord that the method could be correctly compiled and run even if javac throws the error.

like image 1
Ján Halaša Avatar answered Oct 30 '22 17:10

Ján Halaša