Eclipse/javac不同意使用默认方法冲突编译签名;谁是对的?

2022-09-03 02:20:55

下面是一个演示该问题的简单类:

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) 愉快地编译了这个类,而 javac (JDK 1.8.0_121) 吐出了以下编译错误:

$ 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

我相信Eclipse在这种情况下是正确的,但我并不完全确定。根据我对“继承抽象和默认”错误的理解,我认为它应该只在编译实现这两个接口的实际声明类时生成。看起来javac可能正在底层生成一个中间类来处理该通用签名,并错误地将其置于默认方法碰撞测试之下?


答案 1

Javac 根据 JLS 9.4.1.3 是正确的。使用覆盖等效签名继承方法>接口

如果一个接口继承了一个默认方法,该方法的签名与 继承的另一个方法覆盖等效,则会发生编译时错误。(无论其他方法是抽象方法还是默认方法,情况都是如此。II

小字解释:

[...]当抽象和具有匹配签名的默认方法被继承时,我们会产生错误。在这种情况下,可以优先考虑其中一个 - 也许我们会假设默认方法也为抽象方法提供了合理的实现。但这是有风险的,因为除了巧合的名称和签名之外,我们没有理由相信默认方法的行为与抽象方法的契约一致 - 默认方法在最初开发子接口时甚至可能不存在。在这种情况下,更安全的做法是要求用户主动断言默认实现是合适的(通过重写声明)。

相反,类中继承的具体方法的长期行为是它们覆盖接口中声明的抽象方法(参见§8.4.8)。关于潜在协定违规的相同论点也适用于此,但在这种情况下,类和接口之间存在固有的不平衡。为了保持类层次结构的独立性,我们更愿意通过简单地优先考虑具体方法来最小化类接口冲突。

也与 8.4.8.4 进行比较。类>继承具有重写等效签名的方法

如果类 C 继承了一个默认方法,该方法的签名与 C 继承的另一个方法进行重写等效,则会出现编译时错误,除非存在一个在 C 的超类中声明并由 C 继承的抽象方法,该方法与这两个方法进行重写等效。

当在超类中声明抽象方法时,会对严格的默认-抽象和默认-默认冲突规则进行此例外:来自超类层次结构的抽象性断言实质上胜过默认方法,使默认方法的行为就像它是抽象的一样。但是,类中的抽象方法不会重写默认方法,因为仍允许接口优化来自类层次结构的抽象方法的签名。

用更简单的话来说:假设这两个接口在逻辑上是无关的,并且都指定了某种行为契约。因此,假定 中的默认实现是 合同的有效履行是不安全的。抛出错误并让开发人员对其进行整理更安全。Interface2Interface1

我没有在JLS中找到一个可以完全解决您的情况的地方,但我认为错误在于上述规范的要点 - 您声明应该采用一个同时实现和.但对于这样的对象,只有当“存在一个在C的超类中声明并由C继承的抽象方法,该方法与两个方法具有覆盖等价性”时,它才有效。泛型声明不指定有关 的超类的任何内容,因此编译器将其视为“抽象-默认”冲突。extractName()Interface1Interface2X


答案 2

Eclipse是对的

我没有在Java Bug数据库中找到这个javac错误,因此报告了它:JDK-8186643

斯蒂芬·赫尔曼(Stephan Herrmann)的更好解释(见下面的评论):

正确,仅当交叉点类型格式不正确,因此交叉点为空时,才应针对交叉点类型报告错误。但正如这个答案所显示的那样,交叉点不是空的,因此应该是合法的。实际上,错误消息是没有意义的,因为在这一点上没有人提到INT#1,我们只有两个接口的交集,并且该交集仅用作绑定,而不是类型。class INT#1 inherits ...

实现同一方法的多个接口的类可以使用两个编译器进行编译,即使一个接口的方法具有默认实现也是如此。只要 I1I2 都没有等名方法的默认实现,就可以引用该类。仅当两个接口中的一个具有默认实现 javac 时,javac 才会失败。<T extends I1 & I2>

如果歧义应该应用哪个实现,则在定义类时应该已经发生错误,而不是当类被称为<T扩展...>(参见 JLS 4.9.交叉点类型)。

请参阅以下示例,该示例适用于 和 ,但与 和 javac 一起使用时失败:<T extends I1 & I2><T extends IDefault><T extends I1 & IDefault>

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();
    }

}

推荐