Ошибка компиляции 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(); }