Unrelated defaults inheritance error for type variables: why?

試著忘記壹切 提交于 2019-11-27 13:38:43

问题


Disclaimer: this is not about this case (while error sounds as same): class inherits unrelated defaults for spliterator() from types java.util.Set and java.util.List

and here is why:

consider two interfaces (in package "a")

interface I1 {
    default void x() {}
}

interface I2 {
    default void x() {}
}

It is definitely clear to me why we can not declare such class as:

abstract class Bad12 implements I1, I2 {
}

(!) But I can not understand this restriction with reference to type variables:

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

with error: class java.lang.Object&a.I1&a.I2 inherits unrelated defaults for x() from types a.I1 and a.I2.

Why I can not define such type variable? Why java cares about unrelated defaults in this case? What such type variable could "break"?

UPDATE: Just for clarification. I can create several classes of the form:

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

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

and even

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

and so on. Why I can not declare special kind of type variable for such group of classes?

UPD-2: BTW I didn't find any distinct restriction for this case in JLS. It would be nice to confirm your answer with references to JLS.

UPD-3: Some of users told that this code is compiled well in Eclipse. I can not check it, but I checked with javac and got this error:

 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:


This is simply a bug. It turns out that the bug starts in the specification, and then spills over into the implementation. Spec bug is here: https://bugs.openjdk.java.net/browse/JDK-7120669

The constraint is perfectly valid; it is clearly possible that there exist types T that extend both I1 and I2. The problem is how we validate the well-formedness of such types.




回答2:


The mechanics of "intersection types" (a type specified by the union of several interfaces) may seem strange at first, especially when paired with the functioning of generics and type erasure. If you introduce several type bounds with interfaces that do not extend each other, only the first is used as the erasure. If you have code like this:

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
}

Then the generated bytecode would not be able to use Iterable in the erasure of T. The actual erasure of T would be just Comparable, and the generated bytecode would contain casts to Iterable as appropriate (emitting a checkcast to Iterable in addition to the usual invokeinterface opcode), resulting in code conceptually equivalent to the following, with the only difference that the compiler also checks the Iterable<String> bound:

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
}

The problem in your example with override-equivalent methods in the interfaces is that, even though valid types may exist that conform to your requested type bounds (as stated in the comments), the compiler cannot use that truth in any meaningful way due to the presence of at least one default method.

Consider the example class X that implements both I1 and I2 by overriding the defaults with its own method. If your type bound asked for extends X instead of extends I1&I2 the compiler would accept it, erasing T to X and inserting invokevirtual X.f() instructions at each usage of f. However, with your type bound, the compiler would erase T to I1. Since the "join type" X is not real in this second case, on each usage of t.f() the compiler would need to insert either invokeinterface I1.f() or invokeinterface I2.f(). Since the compiler cannot possibly insert a call to "X.f()", even when it logically knows that it is possible for a type X to implement I1&I2, and that any such X must declare that function, it cannot decide between the two interfaces and has to bail out.

In the specific case without any default methods, the compiler can simple call either function, since in that case it knows that either invokeinterface call will unambiguously have been implemented in a single function in any valid X. However, when default methods enter the picture this solution can no longer assumed to produce valid code when partial compilation is taken into account. Consider the following three files:

// 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 is compiled first, with its code as shown, producing "v1.0" of the interfaces A.I1 and A.I2
  • B.java is compiled next, generating an (at that point) valid class implementing the interfaces
  • C.java can now be compiled, again with its code as shown, and the compiler accepts it. It prints what you would expect.
  • The default methods in A.java are uncommented and the file is recompiled (producing "v.1.1" of the interfaces), but B.java is not rebuilt against it. This is akin to upgrading the JRE core libraries but not some other library you are using that implements some JRE interfaces.
  • Finally, we try to rebuild C.java because we are going to use the fancy new features of the latest JRE. Regardless of whether or not we uncomment the call to getI, the intersection type declaration is rejected by the compiler with the same error you asked about.

If the compiler were to accept the intersection type (A.I1 & A.I2) as valid when building C.class the second time, it would take the risk that existing classes like B would raise an IncompatibleClassChangeError at runtime, since a call to getI anywhere would not be resolved either in B or in Object, and the default method search would find two different default methods. The compiler is protecting you against a possible runtime error by disallowing the offending type bound.

Note, however, that the error may still occur if the bound is replaced by T extends B. However, I would consider this final point to be a compiler bug, since the compiler can now see that B implements A.I1, A.I2 with their default methods with override-equivalent signatures, but does not override them, thus ensuring a conflict.

Major edit: removed the first (possibly confusing) example and added an explanation+example on why the particular case with defaults is disallowed.




回答3:


Your question is : Why I can not declare special kind of type variable for such group of classes?

The answer is: because in your group of classes <T extends I1&I2> void x() has two default implementations. Any concrete implementation of the type variable has to override those defaults.

Your A1 and A2 have different (but override-equivalent) definitions of void x().

Your A0 is an overridden definition of void x() which replaces the defaults.

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 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.

JLS 4.4 A type variable must not at the same time be a subtype of two interface types which are different parameterizations of the same generic interface, or a compile-time error occurs.

JLS 4.9 Every intersection type T1 & ... & Tninduces a notional class or interface for the purpose of identifying the members of the intersection type, as follows:

• For each Ti (1 ≤ i ≤ n), let Ci be the most specific class or array type such that Ti <: Ci. Then there must be some Ck such that Ck <: Ci for any i (1 ≤ i ≤ n), or a compile-time error occurs.



来源:https://stackoverflow.com/questions/34644237/unrelated-defaults-inheritance-error-for-type-variables-why

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!