Подтвердить что ты не робот

Неродственная ошибка наследования по умолчанию для переменных типа: почему?

Отказ от ответственности: это не об этом случае (в то время как ошибка звучит так же): класс наследует несвязанные значения по умолчанию для spliterator ( ) из типов java.util.Set и 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&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: BTW Я не нашел каких-либо особых ограничений для этого случая в 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
4b9b3361

Ответ 1

Это просто ошибка. Оказывается, ошибка начинается в спецификации, а затем перетекает в реализацию. Ошибка здесь: https://bugs.openjdk.java.net/browse/JDK-7120669

Ограничение вполне допустимо; очевидно, что существуют типы T, которые распространяются как на I1, так и на I2. Проблема заключается в том, как мы проверяем правильность таких типов.

Ответ 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
}

Тогда сгенерированный байт-код не сможет использовать Iterable в стирании T. Фактическое стирание T будет просто сопоставимым, и сгенерированный байт-код будет содержать приведение в соответствие Iterable (испускание a checkcast to Iterable in дополнение к обычному коду invokeinterface), в результате чего код концептуально эквивалентен следующему, с той лишь разницей, что компилятор также проверяет привязку Iterable<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, переопределяя значения по умолчанию своим собственным методом. Если ваш тип связал запрос extends X вместо extends I1&I2, компилятор примет его, стирая T до X и вставляя инструкции invokevirtual X.f() при каждом использовании f. Однако с привязкой вашего типа компилятор будет стирать T до I1. Поскольку "тип соединения" X не является реальным в этом втором случае, при каждом использовании t.f() компилятор должен будет вставить либо invokeinterface I1.f(), либо invokeinterface I2.f(). Поскольку компилятор не может вставить вызов в "Xf()", даже если он логически знает, что для типа X возможно реализовать I1 и am2; и что любой такой X должен объявить эту функцию, он не может решить между двумя интерфейсов и должен выручить.

В конкретном случае без каких-либо методов 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 сначала скомпилирован с его кодом, как показано, создавая "v1.0" интерфейсов A.I1 и A.I2
  • B.java скомпилируется следующим образом, создавая действительный класс (в этой точке), реализующий интерфейсы
  • Теперь C.java может быть скомпилирован, снова с кодом, как показано, и компилятор принимает его. Он печатает то, что вы ожидаете.
  • Методы по умолчанию в A.java не комментируются, и файл перекомпилируется (производя "v.1.1" интерфейсов), но B.java не перестраивается. Это похоже на обновление основных библиотек JRE, но не на какой-либо другой библиотеке, которую вы используете, которая реализует некоторые интерфейсы JRE.
  • Наконец, мы пытаемся перестроить C.java, потому что мы будем использовать причудливые новые функции последней JRE. Независимо от того, расторгли ли мы вызов getI, объявление типа пересечения отклоняется компилятором с той же ошибкой, о которой вы просили.

Если компилятор должен был принять тип пересечения (A.I1 & A.I2) как действительный при создании C.class во второй раз, рискованно, что существующие классы, такие как B, будут поднимать IncompatibleClassChangeError во время выполнения, поскольку вызов getI нигде не будет разрешено ни в B, ни в объекте, а поиск по умолчанию по умолчанию обнаружит два разных метода по умолчанию. Компилятор защищает вас от возможной ошибки времени выполнения, запрещая ограничение типа нарушения.

Обратите внимание, однако, что ошибка может по-прежнему возникать, если привязка заменяется на T extends B. Тем не менее, я считаю, что этот последний момент является ошибкой компилятора, поскольку теперь компилятор может видеть, что B implements A.I1, A.I2 с их методами по умолчанию с переопределяющими эквивалентными сигнатурами, но не переопределяет их, тем самым обеспечивая конфликт.

Основное изменение: удалил первый (возможно, запутанный) пример и добавил пример объяснения +, почему конкретный случай со значениями по умолчанию запрещен.

Ответ 3

Ваш вопрос: Почему я не могу объявить специальный тип переменной типа для такой группы классов?

Ответ: потому что в вашей группе классов <T extends I1&I2> 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 и... и Tn индуцирует условный класс или интерфейс с целью идентификации членов типа пересечения следующим образом:

• Для каждого Ti (1 ≤ я ≤ n) пусть Ci - наиболее специфический класс или тип массива, так что Ti <: Ci. Тогда должно быть некоторое Ck такое, что Ck <: Ci для любого я (1 ≤ я ≤ n) или возникает ошибка времени компиляции.