Почему сравнение 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- мне не нужно сначала проверять значение нуля.
Джош М.
15

Ваш пример NPE эквивалентен этому коду благодаря автобоксу :

if ( i.intValue( ) == 0 )

Следовательно, NPE, если iесть null.

Александр Погребняк
источник
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;
}
Сатиш Хавалппагол
источник