Почему эта программа 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)

Ответ 1

Спасибо за отчет. Это похоже на ошибку. Я позабочусь об этом и, вероятно, добавлю лучший ответ, если у нас появится больше информации о том, почему это происходит. Я зарегистрировал эту запись ошибки JDK-8043926, чтобы ее отслеживать.

Ответ 2

Спецификация языка 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:

Выражение создания экземпляра класса представляет собой выражение poly (§15.2), если оно использует форму алмаза для аргументов типа для класса и появляется в контексте назначения или контексте вызова (§5.2, §5.3). В противном случае это отдельное выражение.

Возвращаясь к вашему заявлению. Соответствующая часть JLS8 снова §18.5.2. Тем не менее, я не могу сказать, правилен ли следующий оператор в соответствии с JLS8, если компилятор прав с сообщением об ошибке. Но, по крайней мере, у вас есть альтернативы и указатели для получения дополнительной информации.

Acceptor<?> acceptor = new Acceptor<>(new Impl());

Ответ 3

Ввод типа был изменен в 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, но я бы назвал это ошибкой хотя бы по духу.

FYI, это работает (обратите внимание, что у моего Acceptor нет ограничений типа, которые у вас есть):

Acceptor<?> a = new Acceptor<>(new Impl2());

Обратите внимание, что ваш пример использует подстановочный шаблон вне параметра метода (что нецелесообразно), интересно, будет ли та же проблема в более типичном коде, который использует оператор алмаза при вызове метода. (Возможно.)