Разница в поведении тернарного оператора в JDK8 и JDK10

Рассмотрим следующий код

public class JDK10Test {
    public static void main(String[] args) {
        Double d = false ? 1.0 : new HashMap<String, Double>().get("1");
        System.out.println(d);
    }
}

При работе в JDK8 этот код выводит null, тогда как в JDK10 этот код выводит NullPointerException

Exception in thread "main" java.lang.NullPointerException
    at JDK10Test.main(JDK10Test.java:5)

Байт-код, созданный компиляторами, почти идентичен, за исключением двух дополнительных инструкций, созданных компилятором JDK10, которые связаны с автоупаковкой и, похоже, отвечают за NPE.

15: invokevirtual #7                  // Method java/lang/Double.doubleValue:()D
18: invokestatic  #8                  // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;

Является ли такое поведение ошибкой в ​​JDK10 или преднамеренным изменением, чтобы сделать поведение более строгим?

JDK8:  java version "1.8.0_172"
JDK10: java version "10.0.1" 2018-04-17

person SerCe    schedule 09.06.2018    source источник
comment
У меня нет проблем с запуском этого кода в IntelliJ Java(TM) SE Runtime Environment 18.3 (сборка 10.0.1+10) 2018-04-17   -  person arseniyandru    schedule 09.06.2018
comment
@Radiodef для меня false ? 1.0 : (Double) null выдает NPE для обоих JDK (JDK 1.8.0_144 и JDK 10.0.1), а версия new HashMap<...>... выдает только для JDK 10.0.1.   -  person Turing85    schedule 09.06.2018
comment
Я думаю, что метание - правильное поведение, поэтому я предполагаю, что это ошибка. См. §15.25 в таблице 15.25-D.: тип результата должен быть double, поэтому он должен быть распакован.   -  person Radiodef    schedule 09.06.2018
comment
@Radiodef То же самое и в спецификации Java 8.   -  person Jacob G.    schedule 09.06.2018
comment
Окончательный ответ: twitter.com/BrianGoetz/status/1005781178807439362.   -  person SerCe    schedule 12.06.2018


Ответы (2)


Я считаю, что это была ошибка, которая, кажется, была исправлена. Согласно 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, что опять же означает правильность распаковки и выбрасывания.)

Если тип возвращаемого значения метода 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() считается "числовым выражением". Единственная разница между методами заключается в привязке переменной типа.

Вероятно, можно было бы провести больше исследований, но я хотел бы опубликовать свои выводы. Я пробовал довольно много вещей и обнаружил, что ошибка (т.е. без распаковки/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"));
    }
}

Это показывает, что поведение компилятора на самом деле различается в зависимости от того, присваивается ли тернарное выражение локальной переменной или параметру метода.

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

person Radiodef    schedule 09.06.2018

JLS 10, похоже, не указывает никаких изменений в условном операторе, но у меня есть теория.

Согласно JLS 8 и JLS 10, если второе выражение (1.0) имеет тип double, а третье (new HashMap<String, Double>().get("1")) имеет тип Double, то результат условного выражения имеет тип double. JVM в Java 8 кажется достаточно умной, чтобы знать, что, поскольку вы возвращаете Double, нет причин сначала распаковывать результат HashMap#get в double, а затем обратно в Double (потому что вы указали Double).

Чтобы доказать это, замените Double на double в вашем примере, и будет выброшено NullPointerException (в JDK 8); это потому, что сейчас происходит распаковка, и null.doubleValue() явно выдает NullPointerException.

double d = false ? 1.0 : new HashMap<String, Double>().get("1");
System.out.println(d); // Throws a NullPointerException

Кажется, это было изменено в 10, но я не могу сказать вам, почему.

person Jacob G.    schedule 09.06.2018
comment
› ...результат условного выражения имеет тип double Тип должен быть Double, тип оболочки, а не примитивный тип. Другими словами, 1.0 упаковано, null не распаковано. - person Abhijit Sarkar; 09.06.2018
comment
@AbhijitSarkar Согласно JLS 8 и JLS 10, возвращаемый тип условного выражения в этом случае — double, но он упакован в Double, потому что это указал OP. - person Jacob G.; 09.06.2018