【问题标题】:Unrelated defaults inheritance error for type variables: why?类型变量不相关的默认继承错误:为什么?
【发布时间】:2016-01-06 22:41:15
【问题描述】:

免责声明:这不是关于这个案例(虽然错误听起来一样):class inherits unrelated defaults for spliterator() from types java.util.Set and java.util.List

原因如下:

考虑两个接口(在包“a”中)

interface I1 {
    default void x() {}
}

interface I2 {
    default void x() {}
}

我很清楚为什么我们不能声明这样的类:

abstract class Bad12 implements I1, I2 {
}

(!) 但是我无法理解参考类型变量的这个限制:

class A<T extends I1&I2> {
    List<T> makeList() {
        return new ArrayList<>();
    }
}

出现错误:class java.lang.Object&amp;a.I1&amp;a.I2 inherits unrelated defaults for x() from types a.I1 and a.I2

为什么我不能定义这样的类型变量?为什么java 在这种情况下关心不相关的默认值?什么样的类型变量可以“破坏”?

更新:只是为了澄清。我可以创建多个表单类:

class A1 implements I1, I2 {
    public void x() { };
}

class A2 implements I1, I2 {
    public void x() { };
}

甚至

abstract class A0 implements I1, I2 {
    @Override
    public abstract void x();
}

等等。为什么我不能为这样的类组声明特殊类型的变量?

UPD-2: 顺便说一句,我在 JLS 中没有发现针对这种情况的任何明显限制。最好通过引用 JLS 来确认您的答案。

UPD-3: 一些用户说这段代码在 Eclipse 中编译得很好。我无法检查它,但我检查了javac 并得到了这个错误:

 error: class INT#1 inherits unrelated defaults for x() from types I1 and I2
class A<T extends I1&I2> {
        ^
  where INT#1 is an intersection type:
    INT#1 extends Object,I1,I2
1 error

【问题讨论】:

  • 如果这被认为是一个错误,我会吃掉我收集的节日帽子。
  • @pvg 请发布吃帽子的视频!类型约束 完全有效;它的意思是“A 只能用 I1 和 I2 的子类型来实例化。编译器无法证明不存在这样的类型,因为实际上可以构造这样的类型。
  • @pvg 边界 只是一个声明,仅使用 Y 和 Z 的子类型来实例化 A 是合法的。如果 Y&Z 不是,编译器将阻止一个合法的类型(例如,Y 和 Z 都是类),并且如果它可以证明不存在这样的类型,则有权拒绝(例如,Y 是不实现 Z 的最终类)。但边界 I1&I2 是有效的。默认方法不应该参与这个计算——那只是一个错误。
  • @Andremoniy 原来是这个现有错误的副本:bugs.openjdk.java.net/browse/JDK-7120669
  • 无论如何,我们正在等待来自@pvg 的吃帽子的视频:-)

标签: java java-8 type-variables


【解决方案1】:

这只是一个错误。事实证明,错误始于规范,然后溢出到实现中。规范错误在这里:https://bugs.openjdk.java.net/browse/JDK-7120669

约束完全有效;很明显可能存在同时扩展 I1 和 I2 的类型 T。问题是我们如何验证这些类型的良构性。

【讨论】:

    【解决方案2】:

    “交集类型”(由多个接口的联合指定的类型)的机制起初可能看起来很奇怪,尤其是与泛型和类型擦除的功能结合使用时。如果你引入了几个不相互扩展的接口类型边界,那么只有第一个被用作擦除。如果你有这样的代码:

    public static <T extends Comparable<T> & Iterable<String>>
    int f(T t1, T t2) {
        int res = t1.compareTo(t2);
        if (res!=0) return res;
        Iterator<String> s1 = t1.iterator(), s2 = t2.iterator();
        // compare the sequences, etc
    }
    

    然后生成的字节码将无法在 T 的擦除中使用 Iterable。T 的 实际擦除将只是 Comparable,并且生成的字节码将包含适当的 Iterable 强制转换(发射除了通常的invokeinterface 操作码之外,将checkcast 转换为Iterable),导致代码在概念上等同于以下代码,唯一的区别是编译器还检查Iterable&lt;String&gt; 绑定:

    public static int f(Comparable t1, Comparable t2) {
        int res = t1.compareTo(t2);
        if (res!=0) return res;
        Iterator s1 = ((Iterable)t1).iterator(), s2 = ((Iterable)t2).iterator();
        // compare the sequences, etc
    }
    

    您的示例中在接口中使用替代等效方法的问题是,即使可能存在符合您请求的类型边界的有效类型(如 cmets 中所述),编译器也无法以任何有意义的方式使用该事实由于至少存在一个default 方法。

    考虑通过使用自己的方法覆盖默认值来实现 I1 和 I2 的示例类 X。 如果你的类型绑定要求extends X而不是extends I1&amp;I2,编译器会接受它,将T擦除到X并在每次使用f时插入invokevirtual X.f()指令。但是,由于您的类型绑定,编译器会将 T 擦除为 I1。由于在第二种情况下“连接类型”X 不是真实的,因此在每次使用 t.f() 时,编译器都需要插入 either invokeinterface I1.f()invokeinterface I2.f()。由于编译器不可能插入对“X.f()”的调用,即使它在逻辑上知道类型 X 可能实现 I1&I2,并且任何这样的 X 必须 em> 声明该函数,它无法在两个接口之间做出决定,必须退出。

    在没有任何default 方法的特定 情况下,编译器可以简单地调用任一函数,因为在这种情况下,它知道任一invokeinterface 调用将明确地在单个函数中实现在任何有效的 X 中。但是,当默认方法进入图片时,当考虑部分编译时,该解决方案不再假定生成有效代码。考虑以下三个文件:

    // A.java
    public class A {
        public static interface I1 {
            void f();
            // default int getI() { return 1; }
        }
        public static interface I2 {
            void g();
            // default int getI() { return 2; }
        }
    }
    
    // B.java
    public class B implements A.I1, A.I2 {
        public void f() { System.out.println("in B.f"); }
        public void g() { System.out.println("in B.g"); }
    }
    
    // C.java
    public class C {
        public static <T extends A.I1 & A.I2> void test(T var) {
            var.f();
            var.g();
            // System.out.println(var.getI());
        }
    
        public static void main(String[] args) {
            test(new B());
        }
    }
    
    • 首先编译 A.java,其代码如图所示,生成接口 A.I1 和 A.I2 的“v1.0”
    • 接下来编译 B.java,生成(此时)实现接口的有效类
    • 现在可以编译 C.java,其代码再次如图所示,编译器接受它。它会打印您所期望的内容。
    • A.java 中的默认方法未注释并且文件被重新编译(生成接口的“v.1.1”),但是 B.java 没有针对它重新构建。这类似于升级 JRE 核心库,但不是您正在使用的实现某些 JRE 接口的其他库。
    • 最后,我们尝试重建 C.java,因为我们将使用最新 JRE 的新功能。无论我们是否取消注释对 getI 的调用,编译器都会拒绝交集类型声明,并出现您询问的相同错误。

    如果编译器在第二次构建 C.class 时接受交集类型 (A.I1 &amp; A.I2) 为有效,则存在现有类(如 B)会在以下位置引发 IncompatibleClassChangeError 的风险运行时,因为任何地方对 getI 的调用都不会在 B 或 Object 中解析,默认方法搜索会找到两个不同的默认方法。编译器通过禁止违规类型绑定来保护您免受可能的运行时错误。

    但请注意,如果将绑定替换为T extends B,则仍可能发生错误。但是,我认为最后一点是编译器错误,因为编译器现在可以看到 B implements A.I1, A.I2 及其具有覆盖等效签名的默认方法,但不会覆盖它们,从而确保冲突。

    主要编辑:删除了第一个(可能令人困惑的)示例,并添加了一个解释+示例,说明为什么不允许使用默认值的特定情况。

    【讨论】:

    • 关于擦除的说法不正确,对于&lt;T extends I1&amp;I2&gt;,擦除仍然是I1而不是Object。如果您声明了&lt;T extends Object&amp;I1&amp;I2&gt;,则擦除为Object。但是编译器必须决定在调用T.x() 时是调用I1.x() 还是I2.x(),这是正确的。当两个声明都是 abstract 时,这种情况是相同的,并且只有在调用实际发生时才有意义。
    【解决方案3】:

    你的问题是:为什么我不能为这样的类组声明特殊类型的变量?

    答案是:因为在您的类组中&lt;T extends I1&amp;I2&gt; void x()两个默认实现。类型变量的任何具体实现都必须覆盖这些默认值。

    您的 A1 和 A2 对 void x() 的定义不同(但覆盖等效)。

    您的 A0 是 void x() 的覆盖定义,它替换了默认值。

    class A<T extends I1&I2> {
      List<T> makeList() {
          return new ArrayList<>();
      }
    
      public static void main(String[] args) {
        // You can't create an EE to put into A<> which has a default void x()
        new A<EE>();
      }
    }
    

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

    JLS 4.4 类型变量不能同时是同一个泛型接口的不同参数化的两个接口类型的子类型,否则会发生编译时错误。

    JLS 4.9 每个交集类型 T1 & ... & T 引入一个概念类或接口,用于标识交集类型的成员,如下所示:

    • 对于每个 Ti (1 ≤ i ≤ n),令 Ci 是最具体的类或数组类型,使得 Ti <: ci i n ck>

    【讨论】:

    • 当然可以:abstract class A0 implements I1, I2 { public abstract void x(); }
    • 好的,所以现在您正在强制 A0 的实现覆盖 void x();。哎呀,这和我说的一样。
    • 无论如何,这不是我问题的答案。看上面,甚至@BrienGoetz 告诉 是完全有效的。您正在尝试回答不同的问题,而不是关于我的问题。
    • 有有效的类class A1 implements I1, I2 { public void x() { }; },所以它应该存在类型常量&lt;T extends I1&amp;I2&gt;。你所说的不是答案。
    • @Andremoniy 如果一个类 C 继承了一个默认方法,该方法的签名与 C 继承的另一个方法重写等效,除非在 C 的超类中声明了一个抽象方法,否则这是一个编译时错误并由与这两种方法等效的 C 继承。 docs.oracle.com/javase/specs/jls/se8/html/…
    猜你喜欢
    • 1970-01-01
    • 2016-07-30
    • 1970-01-01
    • 1970-01-01
    • 2015-10-18
    • 1970-01-01
    • 2012-03-06
    • 2014-03-31
    • 2017-02-06
    相关资源
    最近更新 更多