Булевы, условные операторы и автобокс

132

Почему это бросает NullPointerException

public static void main(String[] args) throws Exception {
    Boolean b = true ? returnsNull() : false; // NPE on this line.
    System.out.println(b);
}

public static Boolean returnsNull() {
    return null;
}

пока это не

public static void main(String[] args) throws Exception {
    Boolean b = true ? null : false;
    System.out.println(b); // null
}

?

Кстати, решение заключается в замене falseна, Boolean.FALSEчтобы избежать nullраспаковки в boolean- что невозможно. Но вопрос не в этом. Вопрос в том, почему ? Есть ли ссылки в JLS, которые подтверждают такое поведение, особенно во втором случае?

BalusC
источник
28
вау, автобокс - бесконечный источник ... э ... сюрпризов для java-программиста, не так ли? :-)
leonbloy 07
У меня была аналогичная проблема, и меня удивило то, что она не удалась на виртуальной машине OpenJDK, но работала на виртуальной машине HotSpot ... Напишите один раз, запустите где угодно!
kodu

Ответы:

92

Разница в том, что явный тип returnsNull()метода влияет на статическую типизацию выражений во время компиляции:

E1: `true ? returnsNull() : false` - boolean (auto-unboxing 2nd operand to boolean)

E2: `true ? null : false` - Boolean (autoboxing of 3rd operand to Boolean)

См. Спецификацию языка Java, раздел 15.25. Условный оператор? :

  • Для E1 типами 2-го и 3-го операндов являются Booleanи booleanсоответственно, поэтому применяется это предложение:

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

    Поскольку тип выражения - это boolean, второй операнд должен быть приведен к boolean. Компилятор вставляет код автоматической распаковки во второй операнд (возвращаемое значение returnsNull()), чтобы он стал типом boolean. Это, конечно, вызывает NPE из nullвозвращаемого во время выполнения.

  • Для E2 типы 2-го и 3-го операндов <special null type>(не Booleanкак в E1!) И booleanсоответственно, поэтому не применяется конкретное предложение о типизации ( прочтите их! ), Поэтому применяется последнее предложение «иначе»:

    В противном случае второй и третий операнды относятся к типам S1 и S2 соответственно. Пусть T1 будет типом, который является результатом применения преобразования упаковки к S1, и пусть T2 будет типом, который является результатом применения преобразования упаковки к S2. Тип условного выражения является результатом применения преобразования захвата (§5.1.10) к lub (T1, T2) (§15.12.2.7).

    • S1 == <special null type>(см. §4.1 )
    • S2 == boolean
    • T1 == box (S1) == <special null type>(см. Последний пункт в списке преобразований боксов в §5.1.7 )
    • T2 == box (S2) == `Boolean
    • lub (T1, T2) == Boolean

    Таким образом, условное выражение имеет тип, Booleanи третий операнд должен быть приведен в соответствие Boolean. Компилятор вставляет код автоматической упаковки для третьего операнда ( false). Для второго операнда не требуется автоматическая распаковка, как в E1, поэтому автоматическая распаковка NPE при возврате не требуется null.


Этот вопрос требует аналогичного анализа:

Условный оператор Java?: Тип результата

Берт F
источник
4
Имеет смысл ... я думаю. §15.12.2.7 боль.
BalusC
Это просто ... но только задним числом. :-)
Bert F
@BertF Что делает функцию lubв lub(T1,T2)стенд для?
Компьютерщик
1
@Geek - lub () - наименьшая верхняя граница - в основном ближайший суперкласс, который у них общий; поскольку null (тип «специальный нулевой тип») может быть неявно преобразован (расширен) в любой тип, вы можете рассматривать специальный нулевой тип как «суперкласс» любого типа (класса) для целей lub ().
Bert F
25

Линия:

    Boolean b = true ? returnsNull() : false;

внутренне преобразуется в:

    Boolean b = true ? returnsNull().booleanValue() : false; 

выполнить распаковку; таким образом: null.booleanValue()даст NPE

Это одна из основных ошибок при использовании автобокса. Это поведение действительно задокументировано в 5.1.8 JLS.

Изменить: я считаю, что распаковка связана с тем, что третий оператор имеет логический тип, например (добавлено неявное приведение):

   Boolean b = (Boolean) true ? true : false; 
jjungnickel
источник
2
Почему он пытается распаковать таким образом, когда конечным значением является логический объект?
Эрик Робертсон
16

Из Спецификации языка Java, раздел 15.25 :

  • Если один из второго и третьего операндов имеет тип boolean, а тип другого - Boolean, тогда тип условного выражения является логическим.

Итак, первый пример пытается вызвать Boolean.booleanValue()для того , чтобы преобразовать Booleanв booleanв соответствии с первым правилом.

Во втором случае первый операнд имеет нулевой тип, а второй не является ссылочным типом, поэтому применяется преобразование автобокса:

  • В противном случае второй и третий операнды относятся к типам S1 и S2 соответственно. Пусть T1 будет типом, который является результатом применения преобразования упаковки к S1, и пусть T2 будет типом, который является результатом применения преобразования упаковки к S2. Тип условного выражения является результатом применения преобразования захвата (§5.1.10) к lub (T1, T2) (§15.12.2.7).
axtavt
источник
Это отвечает первому случаю, но не второму.
BalusC 07
Возможно, существует исключение, когда одно из значений равно null.
Эрик Робертсон
@Erick: JLS подтверждает это?
BalusC
1
@Erick: Я не думаю, что это применимо, поскольку booleanэто не ссылочный тип.
axtavt 07
1
И позвольте мне добавить ... вот почему вы должны сделать обе стороны тернарного типа одного и того же типа, с явными вызовами, если это необходимо. Даже если вы запомнили спецификации и знаете, что произойдет, следующий программист, который придет и прочитает ваш код, может не прочитать. По моему скромному мнению, было бы лучше, если бы компилятор просто выдавал сообщение об ошибке в этих ситуациях, а не делал то, что обычным смертным трудно предсказать. Что ж, может быть, есть случаи, когда поведение действительно полезно, но я еще не нашел ни одного.
Джей
0

Мы можем увидеть эту проблему из байтового кода. В строке 3 основного байтового кода, 3: invokevirtual #3 // Method java/lang/Boolean.booleanValue:()Zбоксирующего логического значения null, invokevirtualметода java.lang.Boolean.booleanValue, он, конечно же, выбрасывает NPE.

    public static void main(java.lang.String[]) throws java.lang.Exception;
      descriptor: ([Ljava/lang/String;)V
      flags: ACC_PUBLIC, ACC_STATIC
      Code:
        stack=2, locals=2, args_size=1
           0: invokestatic  #2                  // Method returnsNull:()Ljava/lang/Boolean;
           3: invokevirtual #3                  // Method java/lang/Boolean.booleanValue:()Z
           6: invokestatic  #4                  // Method java/lang/Boolean.valueOf:(Z)Ljava/lang/Boolean;
           9: astore_1
          10: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
          13: aload_1
          14: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
          17: return
        LineNumberTable:
          line 3: 0
          line 4: 10
          line 5: 17
      Exceptions:
        throws java.lang.Exception

    public static java.lang.Boolean returnsNull();
      descriptor: ()Ljava/lang/Boolean;
      flags: ACC_PUBLIC, ACC_STATIC
      Code:
        stack=1, locals=0, args_size=0
           0: aconst_null
           1: areturn
        LineNumberTable:
          line 8: 0
Яньхуэй Чжоу
источник