Почему эта программа на 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 в вызовах методов. (Наверное.)

Другие вопросы по тегам