Почему эта программа на Java 8 не компилируется?
Эта программа прекрасно компилируется в Java 7 (или в Java 8 с -source 7
), но не скомпилируется с Java 8:
interface Iface<T> {}
class Impl implements Iface<Impl> {}
class Acceptor<T extends Iface<T>> {
public Acceptor(T obj) {}
}
public class Main {
public static void main(String[] args) {
Acceptor<?> acceptor = new Acceptor<>(new Impl());
}
}
Результат:
Main.java:10: error: incompatible types: cannot infer type arguments for Acceptor<>
Acceptor<?> acceptor = new Acceptor<>(new Impl());
^
reason: inference variable T has incompatible bounds
equality constraints: Impl
upper bounds: Iface<CAP#1>,Iface<T>
where T is a type-variable:
T extends Iface<T> declared in class Acceptor
where CAP#1 is a fresh type-variable:
CAP#1 extends Iface<CAP#1> from capture of ?
1 error
Другими словами, это обратная несовместимость с исходным кодом между Java 7 и 8. Я прошел через Несовместимость между Java SE 8 и списком Java SE 7, но не нашел ничего, что соответствовало бы моей проблеме.
Итак, это ошибка?
Среда:
$ /usr/lib/jvm/java-8-oracle/bin/java -version
java version "1.8.0"
Java(TM) SE Runtime Environment (build 1.8.0-b132)
Java HotSpot(TM) 64-Bit Server VM (build 25.0-b70, mixed mode)
3 ответа
Спасибо за отчет. Это похоже на ошибку. Я позабочусь об этом и, возможно, добавлю лучший ответ, как только у нас будет больше информации о том, почему это происходит. Я подал эту запись об ошибке JDK-8043926, чтобы отследить ее.
Спецификация языка Java значительно изменилась в отношении вывода типов. В JLS7 вывод типа описан в §15.12.2.7 и §15.12.2.8, тогда как в JLS8 есть целая глава, посвященная главе 18. Вывод типа.
Правила довольно сложные, как в JLS7, так и в JLS8. Трудно сказать различия, но очевидно, что есть различия, как видно из раздела § 18.5.2:
Эта стратегия вывода отличается от Java SE 7 Edition из спецификации языка Java [..].
Однако намерение изменения должно было быть обратно совместимым. Смотрите последний абзац раздела §18.5.2:
[..] Стратегия допускает разумные результаты в типичных случаях использования и обратно совместима с алгоритмом в Java SE 7 Edition в спецификации языка Java.
Я не могу сказать, правда это или нет. Тем не менее, существуют некоторые интересные варианты вашего кода, которые не показывают проблему. Например, следующий оператор компилируется без ошибок:
new Acceptor<>(new Impl());
В этом случае нет целевого типа. Это означает, что выражение создания экземпляра класса не является поли-выражением, а правила для вывода типов проще. Смотри §18.5.2:
Если вызов не является поли-выражением, пусть набор границ B 3 будет таким же, как B 2.
Это также причина, почему следующее утверждение работает.
Acceptor<?> acceptor = (Acceptor<?>) new Acceptor<>(new Impl());
Хотя в контексте выражения есть тип, он не считается целевым типом. Если выражение создания экземпляра класса не встречается ни в выражении присваивания, ни в выражении вызова, то оно не может быть выражением поли. Смотрите §15.9:
Выражение создания экземпляра класса является поли-выражением (§15.2), если оно использует ромбовидную форму для аргументов типа для класса, и оно появляется в контексте присваивания или в контексте вызова (§5.2, §5.3). В противном случае это автономное выражение.
Возвращаясь к вашему заявлению. Соответствующая часть JLS8 снова §18.5.2. Однако я не могу сказать вам, является ли следующее утверждение правильным в соответствии с JLS8, или если компилятор прав с сообщением об ошибке. Но, по крайней мере, у вас есть несколько альтернатив и указателей для получения дополнительной информации.
Acceptor<?> acceptor = new Acceptor<>(new Impl());
Вывод типа был изменен в Java 8. Теперь вывод типа просматривает как целевой тип, так и типы параметров, как для конструкторов, так и для методов. Учтите следующее:
interface Iface {}
class Impl implements Iface {}
class Impl2 extends Impl {}
class Acceptor<T> {
public Acceptor(T obj) {}
}
<T> T foo(T a) { return a; }
Следующее теперь нормально в Java 8 (но не в Java 7):
Acceptor<Impl> a = new Acceptor<>(new Impl2());
// Java 8 cleverly infers Acceptor<Impl>
// While Java 7 infers Acceptor<Impl2> (causing an error)
Это, конечно, дает ошибку в обоих случаях:
Acceptor<Impl> a = new Acceptor<Impl2>(new Impl2());
Это также нормально в Java 8:
Acceptor<Impl> a = foo (new Acceptor<>(new Impl2()));
// Java 8 infers Acceptor<Impl> even in this case
// While Java 7, again, infers Acceptor<Impl2>
// and gives: incompatible types: Acceptor<Impl2> cannot be converted to Acceptor<Impl>
Следующее дает ошибку в обоих, но ошибка отличается:
Acceptor<Impl> a = foo (new Acceptor<Impl2>(new Impl2()));
// Java 7:
// incompatible types: Acceptor<Impl2> cannot be converted to Acceptor<Impl>
// Java 8:
// incompatible types: inferred type does not conform to upper bound(s)
// inferred: Acceptor<Impl2>
// upper bound(s): Acceptor<Impl>,java.lang.Object
Ясно, что Java 8 сделала систему логического вывода умнее. Это вызывает несовместимости? Как правило, нет. Из-за стирания типов на самом деле не имеет значения, какие типы были выведены, пока программа компилируется. Java 8 компилирует все программы Java 7? Так и должно быть, но вы привели случай, когда это не так.
Кажется, что происходит то, что Java 8 плохо обрабатывает символы подстановки. Вместо того, чтобы рассматривать их как отсутствие ограничений, кажется, что они рассматривают их как ограничивающие ограничения, которые оно не может удовлетворить. Я не уверен, что он следует букве JLS, но я бы назвал это ошибкой, по крайней мере, по духу.
К вашему сведению, это работает (обратите внимание, что мой Acceptor
не имеет ограничений типа, которые есть у вас):
Acceptor<?> a = new Acceptor<>(new Impl2());
Обратите внимание, что в вашем примере используется подстановочный тип вне параметра метода (что нежелательно). Интересно, произойдет ли такая же проблема в более типичном коде, который использует оператор diamond в вызовах методов. (Наверное.)