Чому ця програма Java 8 не компілюється?


77

Ця програма чудово компілюється в 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)


4
Acceptor<?> acceptor = new Acceptor<Impl>(new Impl())повинні працювати нормально або Acceptor<Impl> acceptor = new Acceptor<>(new Impl()).
Едвін Далорцо,

Звучить для мене це помилка. Схоже, алмазний оператор розгублений.
Bhesh Gurung

це для мене прекрасно компілюється. за допомогою завантажень з oracle.com/technetwork/articles/java/lambda-1984522.html у січні
Рей Таєк,

2
@AleksandrDubinsky Чому?
ghik

Відповіді:


20

Дякую за звіт. Це схоже на помилку. Я подбаю про це і, можливо, додам кращу відповідь, як тільки ми отримаємо більше інформації про те, чому це відбувається. Я подав цей запис про помилку JDK-8043926 , щоб відстежити його.


40

Специфікація мови Java суттєво змінилася щодо виведення типу . У JLS7 висновок про тип було описано в §15.12.2.7 та §15.12.2.8 , тоді як у JLS8 є ціла глава, присвячена главі 18. Висновок про тип .

Правила досить складні, як у JLS7, так і в JLS8. Важко відрізнити відмінності, але очевидно, що відмінності є, як видно з розділу § 18.5.2 :

Ця стратегія умовиводу відрізняється від випуску Java SE 7 у Специфікації мови 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());

Дякую за ґрунтовне пояснення. Я знав про обхідні шляхи, але вирішив, що маю добре розуміти, чому я отримую помилки компіляції у нашій великій
кодовій

я б припустив, що останнє твердження призведе до помилки, оскільки new Acceptor<?>(...)це не дозволено в жодній версії. якщо що, це помилка у виведенні типу java7
aepurniet

@aepurniet Java 7 робить висновок як new Acceptor<Impl>(...). AFAIK, висновки типу Java 7 ніколи не враховують очікуваний тип результату.
ghik

List<Integer> list = new ArrayList<>();це повинно було б бути, оскільки попереднє твердження не містить достатньо інформації, щоб зробити висновок.
aepurniet

@aepurniet О так, неважливо. Висновок про алмаз відрізняється від висновку про параметр типу методу.
ghik

7

Висновок типу змінено в 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());

Зверніть увагу, що у вашому прикладі використовується підстановочний тип поза параметром методу (що є недоцільним), цікаво, чи відбудеться та сама проблема в більш типовому коді, який використовує алмазний оператор у викликах методів. (Мабуть.)


-1, new Foo<>(..)безумовно, розглядає аргументи конструктора для умовиводу.
Тавіан Барнс,

@TavianBarnes хочете надати посилання?
Олександр Дубінський

@TavianBarnes ти маєш рацію. Схоже, у висновках типу Java 8 розглядаються як параметри, так і цільовий тип як для конструкторів, так і для методів. Трохи несправедливо з Вашої точки зору, однак, за те, що Ви помилилися лише у частині моєї відповіді, і яку я попереджував перед AFAIK.
Олександр Дубінський

Досить справедливо, я видалю -1, як тільки ви відредагуєте відповідь. Java 7 також використовувала параметри конструктора для умовиводу btw.
Тавіан Барнс,

@TavianBarnes Я відредагував свою відповідь. Я багато чому навчився, дякую.
Олександр Дубінський
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.