Почему сравнение 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").
— Эш Бурлаченко

Оператор == когда-то использовался для защиты от 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 double`, то выполняется проверка равенства с плавающей запятой.

Обратите внимание, что двоичное числовое продвижение выполняет преобразование набора значений и преобразование распаковки.

Это объясняет:

Integer i = null;

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

Вот выдержка из Effective Java 2nd Edition, пункт 49: Предпочитайте примитивы упакованным примитивам :

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

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

Рекомендации

Связанные вопросы

Связанные вопросы


2
Что касается того, почему someRef == 0 всегда используется числовое сравнение, это очень разумный выбор, поскольку сравнение ссылок двух упакованных примитивов почти всегда является ошибкой программиста. В этом случае было бы бесполезно по умолчанию ссылаться на сравнения.
— Марк Петерс

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


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

i - это целое число, а 0 - это целое число, поэтому в действительности делается что-то вроде этого

i.intValue() == 0

И это вызывает nullPointer, потому что i имеет значение null. Для 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распаковка будет бросатьNullPointerException .

Рассуждения звучат так:

Первое предложение JLS § 15.21.1 Операторы числового равенства == и! = Читается так:

Если оба операнда оператора равенства имеют числовой тип или один имеет числовой тип, а другой может быть преобразован (§5.1.8) в числовой тип, двоичное числовое продвижение выполняется для операндов (§5.6.2).

Очевидно, что он iможет быть преобразован в числовой тип и 0является числовым типом, поэтому двоичное числовое продвижение выполняется для операндов.

В § 5.6.2 Рекламное продвижение двоичных чисел (среди прочего) говорится:

Если какой-либо из операндов имеет ссылочный тип, выполняется преобразование распаковки (§5.1.8).

§ 5.1.8 Преобразование распаковки говорит (среди прочего):

Если r равно нулю, преобразование распаковки вызываетNullPointerException


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.