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


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 якісь посилання, які підтверджують таку поведінку, особливо щодо другого випадку?


28
Нічого, автобоксинг - це нескінченне джерело ... е ... сюрпризів для програміста Java, чи не так? :-)
leonbloy

У мене була подібна проблема, і мене здивувало те, що він не вдався до OpenJDK VM, але працював над HotSpot VM ... Пишіть один раз, бігайте куди завгодно!
коду

Відповіді:


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, 2-й операнд повинен бути примушений до boolean. Компілятор вставляє код авторозпакування у 2-й операнд (повернене значення returnsNull()), щоб змусити його набрати boolean. Це, звичайно, призводить до того, що NPE nullповертається під час виконання.

  • Для E2 типи 2-го та 3-го операндів є <special null type>(не Booleanяк у E1!) І booleanвідповідно, тому не застосовується жодна конкретна клавіатурна пропозиція ( іди читати 'em! ), Тож застосовується заключний пункт "інакше":

    В іншому випадку другий і третій операнди мають типи S1 і S2 відповідно. Нехай T1 - тип, який є результатом застосування перетворення боксу до S1, а T2 - тип, який є результатом застосування перетворення боксу до S2. Тип умовного виразу є результатом застосування перетворення захоплення (§5.1.10) до лубу (T1, T2) (§15.12.2.7).

    • S1 == <special null type>(див. §4.1 )
    • S2 == boolean
    • T1 == поле (S1) == <special null type>(див. Останній пункт у списку перетворень боксу в §5.1.7 )
    • T2 == вікно (S2) == `Булева
    • луб (Т1, Т2) == Boolean

    Отже, тип умовного виразу є, Booleanі 3-й операнд повинен бути примушений до Boolean. Компілятор вставляє код автобоксу для 3-го операнда ( false). 2-й операнд не потребує автоматичного розпакування, як в E1, тому немає автоматичного розпакування NPE при nullповерненні.


Це питання потребує аналогічного типу аналізу:

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


4
Має сенс ... я думаю. §15.12.2.7 біль.
BalusC

Це легко ... але лише заднім числом. :-)
Берт F

@BertF Що робить функцію lubв lub(T1,T2)стенд для?
Geek

1
@Geek - lub () - найменша верхня межа - в основному найближчий суперклас, який у них є спільним; оскільки null (тип "спеціальний тип null") може бути неявно перетворений (розширений) у будь-який тип, ви можете вважати спеціальний 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; 

2
Чому він намагається розв’язати так, коли кінцевим значенням є булевий об'єкт?
Ерік Робертсон

16

З специфікації мови Java, розділ 15.25 :

  • Якщо один з другого та третього операндів має тип булевого типу, а другий тип булевого типу, то тип умовного виразу булевий.

Отже, перший приклад намагається викликати Boolean.booleanValue()для того , щоб перетворити Booleanв booleanвідповідно з першим правилом.

У другому випадку перший операнд має нульовий тип, коли другий не є еталонним типом, тому застосовується перетворення автобоксу:

  • В іншому випадку другий і третій операнди мають типи S1 і S2 відповідно. Нехай T1 - тип, який є результатом застосування перетворення боксу до S1, а T2 - тип, який є результатом застосування перетворення боксу до S2. Тип умовного виразу є результатом застосування перетворення захоплення (§5.1.10) до лубу (T1, T2) (§15.12.2.7).

Це відповідає першому випадку, але не другому.
BalusC

Напевно, є виняток, коли є одне із значень null.
Ерік Робертсон

@Erick: чи підтверджує це JLS?
BalusC

1
@Erick: Я не думаю, що це застосовно, оскільки booleanце не посилання.
axtavt

1
І чи можу я додати ... саме тому вам слід зробити обидві сторони потрійного однотипного типу, якщо потрібно, явні дзвінки. Навіть якщо у вас запам'ятовуються характеристики і знаєте, що буде, наступний програміст прийде і прочитає ваш код. На мою скромну думку, було б краще, якби компілятор просто створив повідомлення про помилку в цих ситуаціях, а не робив речі, які звичайному смертному важко передбачити. Ну, можливо, бувають випадки, коли поведінка справді корисна, але я її ще не вдарив.
Jay

0

Ми можемо бачити цю проблему з байтового коду. У рядку 3 байтового коду основного 3: invokevirtual #3 // Method java/lang/Boolean.booleanValue:()Z, invokevirtualметоду java.lang.Boolean.booleanValue, бокс-булевому значенні null, метод , він, звичайно, кине 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.