【问题标题】:Eclipse/javac disagree on compiling signature with default method collision; who is right?Eclipse/javac 不同意使用默认方法冲突编译签名;谁是对的?
【发布时间】:2018-01-29 14:16:36
【问题描述】:

这是一个演示问题的简单类:

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 可能在后台生成一个中间类来处理该通用签名并错误地对其进行默认方法冲突测试?

【问题讨论】:

  • 由于游戏的目的是编写 javac 可以编译的东西,根据定义,javac 是正确的,除非你遇到了一个不起眼的错误,就像这里发生的那样:stackoverflow.com/questions/42485052/…
  • @slim 我不确定是谁定义了你所指的两个游戏。 Java 是由 JLS 而不是 javac 定义的 :) 任何编译器的开发人员都依赖于报告此类错误的用户来修复他们各自的编译器。直到 s.o.证明 howlger 的回答是错误的,这确实应该被视为 javac 中的一个错误。
  • @BonusLord,您关于幕后中级阶级的假设对我来说非常有说服力。它将准确地解释我们所看到的。
  • 任何仍有疑问的人可能想在stackoverflow.com/questions/34644237/…阅读 Brian Goetz 的回答

标签: java eclipse generics java-8 javac


【解决方案1】:

根据 JLS 9.4.1.3. Interfaces > Inheriting Methods with Override-Equivalent Signatures,Javac 是正确的:

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

小字解释:

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

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

也与8.4.8.4. Classes > Inheriting Methods with Override-Equivalent Signatures比较:

如果一个类 C 继承了一个默认方法,其签名与另一个 C 继承的方法重写等效,则这是一个编译时错误,除非存在一个在 C 的超类中声明并由 C 继承的被重写的抽象方法- 等效于这两种方法。

当在超类中声明抽象方法时,会出现严格的默认抽象和默认默认冲突规则的例外情况:来自超类层次结构的抽象断言本质上胜过默认方法,从而使默认方法表现得好像它是抽象的。但是,类的抽象方法不会覆盖默认方法,因为仍然允许接口细化来自类层次结构的抽象方法的签名。

用更简单的话来说:假设这两个接口在逻辑上是不相关的,并且都指定了某种行为契约。因此,假设Interface2 中的默认实现是Interface1 合同的有效履行是不安全的。抛出错误,让开发者自己解决,这样比较安全。

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

【讨论】:

  • 我不认为规范的这一部分实际上在这里适用,因为第一行:“如果接口'我'继承默认方法......”在我的示例类中,没有'Interface I'(即没有声明的类或接口实际上继承了“getName”方法)所以这条规则不应该发挥作用。 (该规则必须应用于实际满足我的“extractName”方法的参数范围的任何类,但示例案例不包含此类。)
  • @BonusLord 我用类的规范和一些进一步的推理扩展了我的答案。
  • @AdamMichalik 注意,javac 编译一个实现这两个接口的类,但在通用 intersection type 声明中失败(请参阅我的答案中的示例)
【解决方案2】:

Eclipse 是对的

我没有在Java Bug Database 中发现这个javac 错误,因此报告了它:JDK-8186643

Stephan Herrmann 更好的解释(见his comment below):

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

实现同一方法的多个接口的类可以用两种编译器编译,即使一个接口的方法具有默认实现。只要 I1I2 都没有同名方法的默认实现,则该类可以引用为 &lt;T extends I1 &amp; I2&gt;。仅当两个接口之一具有默认实现时 javac 失败。

如果有歧义应该应用哪个实现,错误应该在定义一个类时已经发生,而不是当该类被称为&lt;T extends ...&gt;(参见 JLS 4.9. Intersection Types)。

请参阅以下示例,该示例适用于 &lt;T extends I1 &amp; I2&gt;&lt;T extends IDefault&gt;,但适用于 &lt;T extends I1 &amp; IDefault&gt;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();
    }

}

【讨论】:

  • 对,只有在交叉口类型格式不正确且交叉口为空时才会报告交叉口类型错误。但正如这个答案所示,交叉点不是空的,因此应该是合法的。实际上,错误消息class INT#1 inherits ... 没有任何意义,因为那时没有人提到 class INT#1,我们只有两个 interfaces 的交集,而那个交集仅用作绑定,不用作类型。
  • @StephanHerrmann 感谢我在答案开头插入的解释作为引用。我还添加了指向reported bug 的链接。
  • 感谢错误链接。同时,Dan Smith 找到了这个 SO 线程并添加了一个链接到 bugs.openjdk.java.net/browse/JDK-7120669 - 他提到了 JLS 中使用的“名义类”(“虚构类拐杖”),但关键是 JLS 不强制要求检查概念类,并且 JLS 需要准确说明应该检查的内容(待定)。
  • Brian Goetz in this answer也已确认。
【解决方案3】:

据我了解,问题是关于将已编译类的对象作为参数传递。由于您不能使用抽象类或接口调用 extractName(X) 方法,因此参数对象必须具有解析且明确的 getName() 方法。 Java 使用后期绑定来解析在运行时调用了哪个被覆盖的方法,所以我同意 BonusLord 的观点,即即使javac 抛出错误,该方法也可以正确编译和运行。

【讨论】:

    【解决方案4】:

    我会说这是一个 Javac 错误,或者至少应该是。

    看起来 Javac 的实现者采取了捷径,在泛型边界的实现中重用了创建接口的代码。 IOW,Javac 对待 &lt;X extends I1&amp;I2&gt; 好像是interface X extends I1, I2

    然而,实际上&lt;X extends I1&amp;I2&gt; 是不同的。这只是意味着X 具有I1I2 的方法,但没有说明方法的实现。因此,default 实现是否存在应该是无关紧要的。

    不幸的是,正如@slim 所说,目标是通过 JDK 的编译器,所以 Javac 说了算。提交错误报告,可以吗?

    【讨论】:

    • 是的,我最终将我的实际用例更改为 javac 可以编译的东西,但我对这个问题的主要目标是确定这是否应该报告为针对 Eclipse 的错误与针对 javac 的错误~
    • 这不仅仅是拥有I1I2 的方法,任何替换X 的类型实际上必须是两个接口的(名义)子类型。但是这个答案的本质仍然是正确的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多