Чому порівняння Integer з int може викинути NullPointerException в Java?


81

Мені було дуже заплутано спостерігати за цією ситуацією:

Integer i = null;
String str = null;

if (i == null) {   //Nothing happens
   ...                  
}
if (str == null) { //Nothing happens

}

if (i == 0) {  //NullPointerException
   ...
}
if (str == "0") { //Nothing happens
   ...
}

Отож, як я думаю, операція боксу виконується спочатку (тобто java намагається витягти значення int null), а операція порівняння має нижчий пріоритет, тому викид викидається.

Питання: чому це реалізовано таким чином у Java? Чому бокс має вищий пріоритет, ніж порівняння посилань? Або чому вони не провели перевірку проти nullбоксу?

На даний момент це виглядає непослідовно, коли NullPointerExceptionйого кидають із загорнутими примітивами, а не з істинними типами об’єктів.


Ви отримали б NullPointerException, якби зробили str.equals ("0").
— Ash Burlaczenko

Оператор == колись використовувався для збереження від NPE за будь-яких обставин. Для мене це лише черговий приклад, який демонструє, якою поганою ідеєю було запровадження автоматичного боксу на Java. Це просто не вкладається з такої кількості причин і не пропонує нічого, чого раніше не було. Це лише робить код коротшим, тоді як він затьмарює те, що насправді відбувається.
— x4u

Мої думки відрізняються на 180 градусів. Вони не повинні містити скрізь використовувані примітиви. Тоді нехай компілятор оптимізує та використовує примітиви. Тоді не було б ніякої плутанини.
— MrJacqes

Відповіді:


138

Коротка відповідь

Ключовий момент:

  • == між двома посилальними типами завжди є порівняльне посилання
    • Найчастіше, наприклад, з Integerі String, ви хочете використовувати equalsзамість цього
  • == між посилальним типом і числовим примітивним типом завжди відбувається числове порівняння
    • Тип посилання буде підданий розпаковуванню
    • Розпакування nullзавжди кидаєNullPointerException
  • Хоча Java має багато спеціальних методів лікування String, насправді це НЕ примітивний тип

Наведені вище твердження справедливі для будь-якого даного дійсного коду Java. З таким розумінням у фрагменті, який ви подали, немає жодної суперечливості.


Довга відповідь

Ось відповідні розділи JLS:

JLS 15.21.3 Довідкові оператори рівності ==та!=

Якщо операнди оператора рівності мають як посилальний тип, так і нульовий тип, то ця операція є рівністю об’єкта.

Це пояснює наступне:

Integer i = null;
String str = null;

if (i == null) {   // Nothing happens
}
if (str == null) { // Nothing happens
}
if (str == "0") {  // Nothing happens
}

Обидва операнди є еталонними типами, і тому ==порівняння рівності посилань.

Це також пояснює наступне:

System.out.println(new Integer(0) == new Integer(0)); // "false"
System.out.println("X" == "x".toUpperCase()); // "false"

Щоб ==бути числовою рівністю, принаймні один з операндів повинен мати числовий тип :

JLS 15.21.1 Числові оператори рівності ==та!=

Якщо операнди операції рівності є як числового типу, або один з числових типів і іншій конвертованій для числового типу, двоичная числове розширення виконується над операндами. Якщо рекламований тип операндів - intабо long, то виконується перевірка цілої рівності; якщо рекламований тип float or подвійний`, то виконується перевірка рівності з плаваючою комою.

Зверніть увагу, що двійкове числове просування виконує перетворення набору значень та розпаковування.

Це пояснює:

Integer i = null;

if (i == 0) {  //NullPointerException
}

Ось уривок із Ефективної другої версії Java, Пункт 49: Віддайте перевагу примітивам, ніж коробкам :

Підводячи підсумок, використовуйте примітиви на відміну від примітивів у коробці, коли у вас є вибір. Первісні типи простіші та швидші. Якщо вам потрібно використовувати примітиви в коробці, будьте обережні! Автобокс зменшує багатослівність, але не небезпеку використання примітивних примітивів. Коли ваша програма порівнює два примітиві в коробці з ==оператором, вона виконує порівняння ідентичності, що майже напевно не те, що ви хочете. Коли ваша програма виконує обчислення змішаного типу із залученням примітивів в коробці та без коробки, вона робить розпакування, а коли програма розпаковує, вона може кинути NullPointerException. Нарешті, коли ваша програма встановлює примітивні значення, це може призвести до дорогих і непотрібних створення об’єктів.

Є місця, де вам не залишається іншого вибору, крім як використовувати коробкові примітиви, наприклад, загальні препарати, але в іншому випадку вам слід серйозно подумати, чи рішення щодо використання боксових примітивів виправдане.

Список літератури

Пов’язані запитання

Пов’язані запитання


2
Що стосується того, чому someRef == 0 завжди є числове порівняння, це дуже обґрунтований вибір, оскільки порівняння посилань двох примітованих примітивів майже завжди є помилкою програміста. В даному випадку було б марно замовчувати посилання на порівняння.
— Mark Peters

3
Чому компілятор не замінить вираз (myInteger == 0)на (myInteger != null && myInteger == 0)замість того, щоб покладатися на розробника, щоб написати цей шаблонний код для перевірки нуля? ІМО я повинен мати можливість перевірити, if (myBoolean)і це повинно оцінити, trueякщо і тільки якщо базове значення є конкретним true- мені не потрібно спочатку перевіряти нуль.
— Джош М.


4
if (i == 0) {  //NullPointerException
   ...
}

i - ціле число, а 0 - це int, тому в тому, що насправді робиться, є щось подібне

i.intValue() == 0

І це спричиняє nullPointer, оскільки i є нулем. Для String у нас немає цієї операції, ось чому тут не виняток.


4

Виробники Java могли визначити ==оператора безпосередньо діяти на операнди різних типів, і в цьому випадку Integer I; int i;при порівнянні I==i;можна було б поставити запитання "Чи Iміститься посилання на Integerчиє значення i?" - питання, на яке можна було відповісти без труднощів навіть коли Iмає значення null. На жаль, Java безпосередньо не перевіряє, чи рівні операнди різних типів; натомість він перевіряє, чи дозволяє мова перетворювати тип будь-якого операнда на тип іншого, і - якщо він це робить - порівнює перетворений операнд з неконвертованим. Така поведінка означає, що для змінних x, yі, zз деякими комбінаціями типів, можна мати x==yі y==zалеx!=z[наприклад, x = 16777216f y = 16777216 z = 16777217]. Це також означає, що порівняння I==iперекладається як "Перетворити I на intта, якщо це не створює винятку, порівняй його i".


+1: Насправді намагаючись відповісти на запитання ОП "Чому це створено саме так?"
— Martijn Courteaux

1
@MartijnCourteaux: Багато мов, схоже, визначають оператори лише для операндів відповідних типів, і припускають, що якщо T колись неявно конвертується в U, таке неявне перетворення повинно виконуватися без скарги в будь-який час, коли U можна прийняти, а T не зможе. Якщо б не було такої поведінки, мову можна визначити ==таким чином , що , якщо у всіх випадках , коли x==y, y==z, і x==zвсе компілюється без скарги, три порівняння поводитимуться як відношення еквівалентності. Цікаво, що дизайнери висувають усілякі вигадливі мовні особливості, але ігнорують аксіоматичне дотримання.
— supercat

1

Це через функцію автобоксу Javas . Компілятор виявляє, що в правій частині порівняння ви використовуєте примітивне ціле число, і йому також потрібно розпакувати значення Integer обгортки у примітивне значення int.

Оскільки це неможливо (це нуль, коли ви вишикувались) NullPointerException, викидається.


1

У i == 0Java спробує здійснити автоматичне розпаковування та здійснити числове порівняння (тобто "чи значення, яке зберігається в об’єкті обгортки, посилається на iте саме, що і значення 0?").

Так iяк nullрозпакування буде викинути a NullPointerException.

Міркування виглядають так:

Перше речення JLS § 15.21.1 Числові оператори рівності == і! = Звучить так:

Якщо операнди оператора рівності мають числовий тип, або один має числовий тип, а інший конвертований (§5.1.8) до числового типу, над операндами виконується двійкове числове просування (§5.6.2).

Очевидно, що iвін конвертований у числовий тип і 0є числовим типом, тому двійкове числове просування здійснюється на операндах.

§ 5.6.2 Бінарне числове просування говорить (серед іншого):

Якщо будь-який з операндів має еталонний тип, виконується перетворення розпакування (§5.1.8).

§ 5.1.8 Конверсія розпаковування повідомлень (серед іншого) говорить:

Якщо r має значення null, розпаковування конверсії видає aNullPointerException


0

Просто напишіть метод і зателефонуйте йому, щоб уникнути NullPointerException.

public static Integer getNotNullIntValue(Integer value)
{
    if(value!=null)
    {
        return value;
    }
    return 0;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.