Ответ 1
Я считаю, что это ошибка, которая, похоже, исправлена. По мнению JLS, бросание NullPointerException
кажется правильным.
Я думаю, что здесь происходит то, что по какой-то причине в версии 8 компилятор рассматривал границы переменной типа, указанные методом возвращаемого типа метода, а не фактические аргументы типа. Другими словами, он думает ...get("1")
возвращает Object
. Это может быть связано с тем, что он рассматривает стирание метода или какую-то другую причину.
Поведение должно основываться на возвращаемом типе метода get
, как указано в приведенных ниже отрывках из § 15.26:
Если и второе, и третье выражения операнда представляют собой числовые выражения, условное выражение представляет собой числовое условное выражение.
Для классификации условных выражений следующие выражения представляют собой числовые выражения:
[...]
Выражение вызова метода (§15.12), для которого выбранный наиболее специфический метод (§15.12.2.5) имеет тип возврата, который можно преобразовать в числовой тип.
Обратите внимание, что для универсального метода это тип перед созданием аргументов типа метода.
[...]
В противном случае условное выражение является условным условным выражением.
[...]
Тип числового условного выражения определяется следующим образом:
[...]
Если один из второго и третьего операндов имеет примитивный тип
T
, а тип другого - результат применения преобразования бокса (п. 5.1.7) вT
, то тип условного выражения равенT
Другими словами, если оба выражения конвертируются в числовой тип, а один - примитивен, а другой - в коробке, то результатом типа тернарного условного является примитивный тип.
(Таблица 15.25-C также удобно показывает нам, что тип тройного выражения boolean? double: Double
действительно будет double
, опять же означающим, что unboxing и throwing верны.)
Если возвращаемый тип метода get
не был конвертирован в числовой тип, то тернарный условный будет рассматриваться как "условное условное выражение", и разблокировка не произойдет.
Кроме того, я думаю, что примечание "для универсального метода, это тип перед созданием аргументов типа метода", не должно применяться к нашему делу. Map.get
не объявляет переменные типа, поэтому это не общий метод по определению JLS. Однако эта заметка была добавлена в Java 9 (это единственное изменение, см. JLS8), поэтому возможно, что это имеет какое-то отношение к поведению, которое мы наблюдаем сегодня.
Для HashMap<String, Double>
возвращаемый тип get
должен быть Double
.
Здесь MCVE поддерживает мою теорию о том, что компилятор рассматривает ограничения переменной типа, а не фактические аргументы типа:
class Example<N extends Number, D extends Double> {
N nullAsNumber() { return null; }
D nullAsDouble() { return null; }
public static void main(String[] args) {
Example<Double, Double> e = new Example<>();
try {
Double a = false ? 0.0 : e.nullAsNumber();
System.out.printf("a == %f%n", a);
Double b = false ? 0.0 : e.nullAsDouble();
System.out.printf("b == %f%n", b);
} catch (NullPointerException x) {
System.out.println(x);
}
}
}
Результатом этой программы на Java 8 является:
a == null
java.lang.NullPointerException
Другими словами, несмотря на то, что e.nullAsNumber()
и e.nullAsDouble()
имеют одинаковый фактический тип возврата, только e.nullAsDouble()
рассматривается как "числовое выражение". Единственное различие между методами - это ограничение переменной типа.
Там, вероятно, больше исследований, которые можно было бы сделать, но я хотел опубликовать свои выводы. Я попробовал немало вещей и обнаружил, что ошибка (т.е. Unboxing/NPE), похоже, происходит только тогда, когда выражение представляет собой метод с переменной типа в возвращаемом типе.
Интересно, что я обнаружил, что следующая программа также бросает в Java 8:
import java.util.*;
class Example {
static void accept(Double d) {}
public static void main(String[] args) {
accept(false ? 1.0 : new HashMap<String, Double>().get("1"));
}
}
Это показывает, что поведение компилятора фактически различно, в зависимости от того, назначено ли тернарное выражение локальной переменной или параметру метода.
(Первоначально я хотел использовать перегрузки, чтобы доказать фактический тип, который компилятор передает тернарному выражению, но это не похоже на то, что возможно с учетом вышеупомянутой разницы. Возможно, существует еще один способ, о котором я не думал, хоть.)