Неродственная ошибка наследования по умолчанию для переменных типа: почему?
Отказ от ответственности: это не об этом случае (в то время как ошибка звучит так же): класс наследует несвязанные значения по умолчанию для 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
Ответы
Ответ 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) или возникает ошибка времени компиляции.