Разница в поведении троичного оператора на JDK8 и JDK10 - PullRequest
0 голосов
/ 09 июня 2018

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

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

Ответы [ 2 ]

0 голосов
/ 09 июня 2018

Я считаю, что это была ошибка, которая, кажется, была исправлена.Бросок NullPointerException кажется правильным поведением, согласно JLS.

Я думаю, что здесь происходит то, что по какой-то причине в версии 8 компилятор рассмотрел границы упомянутой переменной типапо типу возврата метода, а не фактические аргументы типа.Другими словами, он думает, что ...get("1") возвращает Object.Это может быть связано с удалением метода или по какой-либо другой причине.

Поведение должно зависеть от типа возврата метода get, как указано в приведенных ниже выдержках из §15.26 :

  • Если и второе, и третье выражения операнда являются числовыми выражениями, условное выражение является числовым условным выражением.

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

    • […]

    • Выражение вызова метода(§15.12), для которого выбранный наиболее конкретный метод (§15.12.2.5) имеет тип возвращаемого значения, который можно преобразовать в числовой тип.

      Обратите внимание, что для универсального метода этоявляется типом перед созданием аргументов типа метода.

    • […]

  • В противном случае, условныйвыражение является условным условным выражениемression.

[…]

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

  • […]

  • Если один из второго и третьего операндов имеет примитивный тип 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"));
    }
}

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

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

0 голосов
/ 09 июня 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, но я не могу сказать вам, почему.

...