Чому порівняння 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.