Ошибка компиляции Generics с тройным оператором в Java 8, но не в Java 7

Этот класс компилируется в Java 7, но не в Java 8:

public class Foo {

    public static void main(String[] args) throws Exception {
        //compiles fine in Java 7 and Java 8:
        Class<? extends CharSequence> aClass = true ? String.class : StringBuilder.class;
        CharSequence foo = foo(aClass);

        //Inlining the variable, compiles in Java 7, but not in Java 8:
        CharSequence foo2 = foo(true ? String.class : StringBuilder.class);

    }

    static <T> T foo(Class<T> clazz) throws Exception {
        return clazz.newInstance();
    }
}

Ошибка компиляции:

Ошибка: (9, 29) java: метод foo в классе Foo не может применяться к данным типы; required: java.lang.Class found: true? Str [...] Класс
Причина: предполагаемый тип не соответствует ограничениям (-ам) равенства     inferred: java.lang.StringBuilder     ограничения равенства: java.lang.StringBuilder, java.lang.String

Почему это перестало работать на Java 8? Является ли это преднамеренным/побочным эффектом какой-либо другой функции, или это просто ошибка компилятора?

Ответ 1

Это не ошибка javac, согласно текущей спецификации. Я написал ответ здесь, для аналогичной проблемы. Здесь проблема более или менее одинаковая.

В условном выражении задания или вызова контекстные выражения представляют собой поли выражения. Это означает, что тип выражения не является результатом применения преобразования захвата в lub (T1, T2), см. JSL-15.25.3 для детального определения T1 и T2. Вместо этого мы имеем также из этой части спецификации, что:

В тех случаях, когда условное условное выражение с полирежиментом появляется в контексте конкретного вид с типом цели T, его второе и третье выражения операнда аналогично появляются в контексте одного и того же типа с типом цели.

Тип условного условного выражения с полирегулярностью совпадает с типом целевого типа.

Таким образом, это означает, что целевой тип переносится в оба операнда условного условного выражения, и оба операнда приписываются этому типу цели. Таким образом, компилятор заканчивает сбор ограничений из обоих операндов, что приводит к неразрешимому набору ограничений и, следовательно, к ошибке.


ОК, но зачем мы получаем оценки равенства для T здесь?

Посмотрим подробно, из вызова:

foo(true ? String.class : StringBuilder.class)

где foo:

static <T> T foo(Class<T> clazz) throws Exception {
    return clazz.newInstance();
}

Мы имеем это, поскольку мы вызываем метод foo() с выражением true ? String.class : StringBuilder.class. Это условное условное выражение должно быть совместимым в свободном контексте вызова с типом Class<T>. Это представлено в виде JLS-18.1.2:

true ? String.class : StringBuilder.class → Class<T>

Как следует из JLS-18.2.1, мы имеем следующее:

Формула ограничения формы <Выражение → T > уменьшается следующим образом:

...

  • Если выражение является условным выражением формы e1? e2: e3, ограничение сводится к двум формулам ограничений, и .

Это означает, что мы получаем следующие формулы ограничений:

String.class → Class<T>
StringBuilder.class → Class<T>

или

Class<String> → Class<T>
Class<StringBuilder> → Class<T>

Позже из JLS-18.2.2 мы имеем следующее:

Формула ограничения формы сводится к следующему:

...

  • В противном случае ограничение сводится к .

Я включаю только связанные части. Итак, теперь у нас есть:

Class<String> <: Class<T>
Class<StringBuilder> <: Class<T>

Из JLS-18.2.3 мы имеем:

Формула ограничения формы сводится следующим образом:

...

  • В противном случае ограничение уменьшается в соответствии с формой T:
    • Если T является параметризованным классом или типом интерфейса или внутренним типом класса параметризованный класс или тип интерфейса (прямо или косвенно), пусть A1,..., An be аргументы типа T. Среди супертипов S соответствующий класс или тип интерфейса, с аргументами типа B1,..., Bn. Если такого типа нет существует, ограничение сводится к false. В противном случае ограничение сводится к следующие новые ограничения: для всех я (1 ≤ я ≤ n), .

Так как Class<T>, Class<String> и Class<StringBuilder> являются параметризованными классами, это означает, что теперь наши ограничения сводятся к:

String <= T
StringBuilder <= T

Также из JLS-18.2.3 мы имеем:

Формула ограничения формы , где S и T являются аргументами типа (§4.5.1), уменьшается следующим образом:

...

  • Если T - тип:
    • Если S является типом, ограничение сводится к .

Таким образом, мы заканчиваем этими ограничениями для T:

String = T
StringBuilder = T

Наконец, JLS-18.2.4 мы имеем следующее:

Формула ограничения формы , где S и T являются типами, уменьшается как следующим образом:

...

  • В противном случае, если T является переменной вывода, α, ограничение сводится к границе S = α.

И нет решения для переменной типа T с ограничениями T = String и T = StringBuilder. Нет никакого типа, который компилятор может заменить Т для того, что удовлетворяет обоим ограничениям. По этой причине компилятор выводит сообщение об ошибке.


Итак, javac в порядке в соответствии с текущей спецификацией, но верно ли это в спецификации? Ну, проблема совместимости между 7 и 8 должна быть исследована. По этой причине я подал JDK-8044053, чтобы мы могли отслеживать эту проблему.

Ответ 2

Я собираюсь выйти на конечность и сказать, что эта ошибка (хотя она может соответствовать или не соответствовать обновленной версии JLS, которую я допускаю, что я не читал подробно) объясняется несогласованностью в обработке типов компилятором JDK 8.

В общем случае тернарный оператор использовал вывод того же типа, что и для метода с двумя аргументами, который имел формальные параметры, основанные на одном и том же параметре. Например:

static <T> T foo(Class<? extends T> clazz, Class<? extends T> clazz2) { return null; }

public static void main(String[] args) {
    CharSequence foo2 = foo(String.class, StringBuilder.class);
}

В этом примере T можно определить как захват ? extends Object & Serializable & CharSequence. Теперь аналогично, в JDK 7, если мы вернемся к вашему первоначальному примеру:

CharSequence foo2 = foo(true ? String.class : StringBuilder.class);

Это делает почти точный вывод того же типа, что и выше, но в этом случае рассмотрим тернарный оператор как метод как таковой:

static <T> T ternary(boolean cond, T a, T b) {
    if (cond) return a;
    else return b;
}

Итак, в этом случае, если вы передаете параметры String.class и StringBuilder.class в качестве параметров, предполагаемый тип T (грубо говоря) Class<? extends Object & Serializable & CharSequence>, что мы и хотели.

Фактически вы можете заменить использование вашего тернарного оператора в исходном фрагменте с помощью этого метода, таким образом:

public class Foo {

    public static void main(String[] args) throws Exception {
        //compiles fine in Java 7 and Java 8:
        Class<? extends CharSequence> aClass = true ? String.class : StringBuilder.class;
        CharSequence foo = foo(aClass);

        //Inlining the variable using 'ternary' method:
        CharSequence foo2 = foo(ternary(true, String.class, StringBuilder.class));

    }

    static <T> T foo(Class<T> clazz) throws Exception {
        return clazz.newInstance();
    }

    static <T> T ternary(boolean cond, T a, T b) {
        if (cond) return a;
        else return b;
    }
}

... И теперь он компилируется в Java 7 и 8 (edit: на самом деле он также не работает с Java 8! снова: теперь он работает, Jdk 8u20), Что дает? по какой-то причине ограничение ограничений теперь накладывается на T (в методе foo), а не на ограничение нижних границ. Соответствующий раздел JLS для Java 7 15.12.2.7; для Java 8 есть целая новая глава о типе вывода (глава 18).

Обратите внимание, что явно набрав T в вызове 'trernary', разрешает компиляцию с Java 7 и 8, но это не похоже, что это необходимо. Java 7 делает правильные вещи, где Java 8 дает ошибку, хотя существует соответствующий тип, который можно сделать для T.

Ответ 3

Проблема возникает только в контексте параметров и назначений. То есть.

CharSequence cs1=(true? String.class: StringBuilder.class).newInstance();

работает. В отличие от других утверждений ответа, использование общего метода <T> T ternary(boolean cond, T a, T b) не работает. Это все еще не выполняется, когда вызов передается на общий метод, например <T> T foo(Class<T> clazz), так что выполняется поиск фактического типа <T>. Однако он работает в примере присваивания

Class<? extends CharSequence> aClass = true ? String.class : StringBuilder.class;

поскольку результирующий тип уже указан явно. Конечно, тип, добавленный в Class<? extends CharSequence>, всегда будет исправлять это для других случаев использования.

Проблема заключается в том, что алгоритм задается как поиск "наименьшей верхней границы" второго и третьего операндов, а затем применяется "преобразование захвата". Первый шаг уже не выполняется для двух типов Class<String> и Class<StringBuilder>, поэтому второй шаг, который рассмотрит контекст, даже не попытался.

Это не удовлетворительная ситуация, однако в этом специальном случае есть изящная альтернатива:

public static void main(String... arg) {
  CharSequence cs=foo(true? String::new: StringBuilder::new);
}

static <T> T foo(Supplier<T> s) { return s.get(); }