Eclipse/javac не согласен с компиляцией подписи с конфликтом метода по умолчанию; кто прав?
Вот простой класс, который демонстрирует проблему:
package com.mimvista.debug;
public class DefaultCollisionTest {
public static interface Interface1 {
public String getName();
}
public static interface Interface2 {
public default String getName() { return "Mr. 2"; };
}
public static <X extends Interface1&Interface2> String extractName(X target) {
return target.getName();
}
}
Eclipse (Neon 2) с радостью компилирует этот класс, а javac (JDK 1.8.0_121) выплевывает следующую ошибку компиляции:
$ javac src/com/mimvista/debug/DefaultCollisionTest.java
src\com\mimvista\debug\DefaultCollisionTest.java:13: error: class INT#1 inherits abstract and default for getName() from types Interface2 and Interface1
public static <X extends Interface1&Interface2> String extractName(X target) {
^
where INT#1 is an intersection type:
INT#1 extends Object,Interface1,Interface2
1 error
Я считаю, что Eclipse в этом случае прав, но я не совсем уверен. Основываясь на моем понимании ошибки "наследует абстрактную и дефолтную", я думаю, что она должна генерироваться только при компиляции реального объявленного класса, который реализует эти два интерфейса. Кажется, что javac может генерировать промежуточный класс под капотом, чтобы справиться с этой общей подписью и ошибочно подвергать его методу столкновений по умолчанию?
Ответы
Ответ 1
Javac корректен согласно JLS 9.4.1.3. Интерфейсы > Методы наследования с переопределяющими эквивалентными сигнатурами:
Если интерфейс I
наследует метод по умолчанию, подпись которого переопределяется эквивалентом с другим методом, унаследованным I
, тогда возникает ошибка времени компиляции. (Это относится к тому, является ли другой метод абстрактным или по умолчанию.)
Небольшая печать объясняет:
[...], когда абстрактный метод и метод по умолчанию с соответствующими сигнатурами наследуются, мы создаем ошибку. В этом случае можно было бы отдать приоритет одному или другому - возможно, мы предположили бы, что метод по умолчанию обеспечивает разумную реализацию абстрактного метода. Но это рискованно, потому что, помимо имени совпадения и подписи, у нас нет оснований полагать, что метод по умолчанию ведет себя последовательно с контрактом абстрактного метода - метод по умолчанию, возможно, даже не существовал, когда исходный интерфейс был первоначально разработан. В этой ситуации безопаснее просить пользователя активно утверждать, что реализация по умолчанию является подходящей (через переопределяющее объявление).
Напротив, давнее поведение для наследуемых конкретных методов в классах заключается в том, что они переопределяют абстрактные методы, объявленные в интерфейсах (см. §8.4.8). Здесь применяется один и тот же аргумент о потенциальном нарушении контракта, но в этом случае существует дисбаланс между классами и интерфейсами. Мы предпочитаем, чтобы сохранить независимый характер иерархии классов, чтобы минимизировать столкновения класса с интерфейсом, просто отдав приоритет конкретным методам.
Также сравните с 8.4.8.4. Классы > Методы наследования с переопределяющими эквивалентными сигнатурами:
Это ошибка времени компиляции, если класс C наследует метод по умолчанию, подпись которого переопределяется эквивалентом с другим методом, унаследованным C, если не существует абстрактного метода, объявленного в суперклассе C и унаследованного C, который переопределен эквивалентно двум методам.
Это исключение для строгих правил абзаца по умолчанию и стандартных по умолчанию правил конфликтов выполняется, когда абстрактный метод объявляется в суперклассе: утверждение абстрактности, исходящее из иерархии суперкласса, по существу превосходит метод по умолчанию, делая метод по умолчанию действовать так, как если бы оно было абстрактным. Тем не менее, абстрактный метод из класса не переопределяет метод по умолчанию, поскольку интерфейсам по-прежнему разрешено уточнять подпись абстрактного метода, исходящего из иерархии классов.
В еще более понятных словах: предположение состоит в том, что два интерфейса логически несвязаны и оба указывают какой-то контракт на поведение. Поэтому небезопасно предполагать, что реализация по умолчанию в Interface2
является действительным выполнением контракта Interface1
. Это безопаснее, чтобы выбросить ошибку и позволить разработчику разобраться.
Я не нашел места в JLS, где он точно справился бы с вашим делом, но я думаю, что ошибка в сущности приведенных выше спецификаций - вы заявляете, что extractName()
следует принимать объект, который реализует как Interface1
, так и Interface2
. Но для такого объекта это было бы справедливо только в том случае, если "существует абстрактный метод, объявленный в суперклассе C и унаследованный C, который эквивалентен переопределению с помощью двух методов". В вашем общем объявлении ничего не указано о суперклассе X
, поэтому компилятор рассматривает его как столкновение с абстрактным значением по умолчанию.
Ответ 2
Eclipse прав.
Я не нашел эту ошибку javac в Java Bug Database и поэтому сообщил об этом: JDK-8186643
Лучшее объяснение Stephan Herrmann (см. его комментарий ниже):
Правильно, сообщение об ошибке в отношении типа пересечения должно происходить только тогда, когда тип пересечения не является корректным и, следовательно, пересечение пусто. Но, как показывает этот ответ, пересечение не пустое и, следовательно, должно быть законным. На самом деле сообщение об ошибке class INT#1 inherits ...
не имеет смысла, потому что в тот момент никто не упоминал class INT # 1, мы имеем только пересечение двух интерфейсов, и это пересечение используется только как граница, а не как тип.
Класс, который реализует несколько интерфейсов одного и того же метода, может быть скомпилирован с обоими компиляторами, даже если метод одного интерфейса имеет реализацию по умолчанию. Класс можно называть как <T extends I1 & I2>
, пока ни I1, ни I2 не имеют реализации по умолчанию для одинаково названного метода. Только если один из двух интерфейсов имеет реализацию по умолчанию, javac терпит неудачу.
В случае двусмысленности, реализация которой должна применяться, ошибка должна возникать при определении класса, а не когда класс называется <T extends ...>
(см. JLS 4.9. Типы пересечений).
См. следующий пример, который работает с <T extends I1 & I2>
и <T extends IDefault>
, но с ошибкой <T extends I1 & IDefault>
и javac:
interface I1 {
String get();
}
interface I2 {
String get();
}
interface IDefault {
default String get() {
return "default";
};
}
public class Foo implements I1, I2, IDefault {
@Override
public String get() {
return "foo";
}
public static void main(String[] args) {
System.out.print(getOf(new Foo()));
}
// static <T extends I1 & IDefault> String getOf(T t) { // fails with javac
static <T extends I1 & I2> String getOf(T t) { // OK
return t.get();
}
}
Ответ 3
Как я понимаю, вопрос заключается в передаче объекта уже скомпилированного класса в качестве параметра. Поскольку вы не можете вызвать метод extractName(X)
с абстрактным классом или интерфейсом, объект аргумента должен иметь метод getName()
, разрешенный и недвусмысленный. Java использует позднюю привязку для разрешения того, какой переопределенный метод вызывается во время выполнения, поэтому я бы согласился с BonusLord, что метод можно корректно скомпилировать и запустить, даже если javac
выдает ошибку.
Ответ 4
Я бы сказал, что это ошибка Javac, или, по крайней мере, это должно быть.
Похоже, что разработчики Javac взяли ярлык и повторно использовали код для создания интерфейса при реализации общей границы. IOW, Javac лечит
<X extends I1&I2>
как будто это было interface X extends I1, I2
.
В действительности, однако, <X extends I1&I2>
отличается. Это просто означает, что X
имеет методы как I1
, так и I2
, но ничего не говорит о реализации методов. Поэтому отсутствие или наличие реализации default
должно быть неактуальным.
К сожалению, как говорит @slim, цель состоит в том, чтобы передать JDK-компилятор, поэтому у Javac есть последнее слово. Отправить отчет об ошибке, возможно?