类型变量的不相关默认值继承错误:为什么?

2022-09-01 21:22:03

免责声明这不是关于这种情况(虽然错误听起来是一样的):类从java.util.Set和java.util.List类型继承了spliterator()的不相关默认值

原因如下:

考虑两个接口(在封装中”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&a.I1&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

答案 1

这只是一个错误。事实证明,bug 从规范开始,然后溢出到实现中。规范错误在这里: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的实际擦除将只是可比的,并且生成的字节码将包含适当的Cast到Iterable(除了通常的操作码之外,还向Iterable发出a),导致代码在概念上等效于以下内容,唯一的区别是编译器还检查边界:checkcastinvokeinterfaceIterable<String>

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
}

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

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

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

// 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());
    }
}
  • 首先编译.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时接受交集类型为有效,则会承担现有类(如 B)在运行时引发 a 的风险,因为对 getI anywhere 的调用在 B 或 Object 中都不会解析,并且默认方法搜索将找到两个不同的默认方法。编译器通过禁止有问题的类型绑定来保护您免受可能的运行时错误的影响。(A.I1 & A.I2)IncompatibleClassChangeError

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

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


推荐