为什么在Java中将Integer与int进行比较可能会抛出NullPointerException?

86

我对观察到的这种情况感到非常困惑:

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试图从null中提取int值),比较操作的优先级较低,这就是为什么会抛出异常的原因。

问题是:为什么Java要实现成这样呢?为什么装箱的优先级比比较引用还要高?或者为什么他们没有在装箱之前对null进行验证呢?

目前看来,当使用包装的原始类型时抛出NullPointerException,而使用真实对象类型时却没有抛出,这显得不一致。


如果你执行str.equals("0"),你将得到一个NullPointerException异常。 - Ash Burlaczenko
"==" 运算符曾经在任何情况下都可防止 NPEs。对我来说,这只是又一个例子,展示了引入Java中自动装箱的坏主意。它不适合许多原因,并且没有提供之前不存在的任何内容。它只会使代码变得更短,同时也掩盖了实际发生的事情。 - x4u
我的想法完全相反。他们不应该在各个地方都使用基本类型的对象。然后让编译器进行优化并使用基本类型。这样就不会有任何混淆了。 - MrJacqes
7个回答

144

简短回答

关键点是:

  • 两个引用类型之间的==比较始终是引用比较。
    • 通常情况下,例如使用IntegerString,您应该使用equals
  • 引用类型和数字原始类型之间的==比较始终是数字比较。
    • 引用类型将被强制拆箱转换。
    • 强制拆箱null始终会抛出NullPointerException
  • 虽然Java对String有许多特殊处理,但实际上它不是原始类型。

以上语句适用于任何给定的有效 Java 代码。有了这个理解,您提供的片段就没有任何不一致之处。


详细回答

以下是相关的JLS部分:

JLS 15.21.3 引用相等运算符 ==!=

如果等式运算符的操作数都是引用类型或null类型中的任何一个,则该操作是对象相等性。

这解释了以下内容:

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 数字等号运算符 ==!=

如果相等运算符的操作数都是数字类型,或者其中一个可以转换为数字类型,则对操作数执行二进制数字升级。 如果操作数的升级类型是intlong,则执行整数等式测试; 如果升级类型是floatdouble,则执行浮点等式测试。

请注意,二进制数字升级执行值集转换和取消装箱转换。

这解释了:

Integer i = null;

if (i == 0) {  //NullPointerException
}
这是《Effective Java 第二版》,第49条款:优先使用原始类型而非装箱基本类型的摘录:

总之,如果可以选择,应该优先使用原始类型而不是装箱基本类型。 原始类型更简单、更快速。 如果必须使用装箱基本类型,请小心! 自动装箱减少了冗余,却并未降低使用装箱基本类型的风险。 当程序使用==运算符比较两个装箱基本类型时,它进行的是标识比较,这几乎肯定不是您想要的。 当程序执行涉及装箱和未装箱基本类型的混合类型计算时,它会进行拆箱;而当程序进行拆箱时,它可能会抛出NullPointerException异常。 最后,当程序装箱基本值时,可能会导致昂贵且不必要的对象创建。

在某些情况下,您别无选择,只能使用装箱基本类型,例如泛型,但除此之外,您应认真考虑是否有理由使用装箱基本类型。

参考文献

相关问题

相关问题


2
关于为什么 someRef == 0 总是数值比较,这是一个非常明智的选择,因为比较两个装箱基元类型的引用几乎总是程序员的错误。在这种情况下默认使用引用比较是无用的。 - Mark Peters
3
为什么编译器不会用 (myInteger != null && myInteger == 0) 替换表达式 (myInteger == 0),而是依赖开发人员编写这些样板代码来进行空值检查?在我看来,如果值确切地为 true,那么我应该能够检查 if(myBoolean) 并且它应该只有在基础值为 true 时才被评估为 true -- 我不应该先进行空值检查。 - Josh M.

15
您的NPE示例与使用自动装箱的以下代码等效:

if (i.intValue( ) == 0)

因此,如果inull,则会引发NPE。

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

i是一个整数,0是一个int类型,所以实际上做的事情就像这样

i.intValue() == 0

这是因为i是null而导致了nullPointer错误。对于String类型,我们没有这个操作,所以就不会出现异常。


4
Java的开发者本来可以定义“==”运算符以直接作用于不同类型的操作数,这样给定“Integer I; int i;”时,比较“I==i;”可以询问“‘I’是否持有一个值为‘i’的‘Integer’引用?”——即使‘I’为空也可以回答这个问题。不幸的是,Java并不直接检查不同类型的操作数是否相等;相反,它检查语言是否允许将任一操作数的类型转换为另一个操作数的类型,并将转换后的操作数与未转换的操作数进行比较。这种行为意味着对于变量x、y和z,具有某些类型组合,可能会出现“x==y”和“y==z”,但“x!=z”的情况[例如,x = 16777216f,y = 16777216,z = 16777217]。这也意味着比较“I==i”被翻译为“将‘I’转换为‘int’,如果没有抛出异常,则将其与‘i’进行比较”。

+1:因为实际上尝试回答了OP的问题:“为什么设计成那样?” - Martijn Courteaux
1
@MartijnCourteaux:许多语言似乎仅为匹配类型的操作数定义运算符,并假定如果T可以隐式转换为U,那么任何时候都应该执行这种隐式转换,而不会有任何投诉,只要U可以被接受但T不能。如果没有这样的行为,一种语言可以以这样的方式定义==,即如果在所有情况下,x==yy==zx==z都能够编译而不会出现问题,则这三个比较将表现为等价关系。有趣的是,设计者们推出各种花哨的语言功能,却忽略了公理遵从性。 - supercat

1
这是因为Java的自动装箱特性。编译器检测到,在比较的右侧,您正在使用一个原始整数,并且需要将包装器Integer值解包为原始int值。
由于这是不可能的(正如您所指出的那样,它是null),因此会抛出NullPointerException异常。

1
在Java中,当执行i == 0时,会尝试自动拆箱并进行数字比较(即“存储在i引用的包装对象的值是否与0相同?”)。
由于inull,因此拆箱操作将抛出NullPointerException异常。
推理如下: JLS § 15.21.1 Numerical Equality Operators == and !=的第一句话如下:

如果等式运算符的操作数都是数字类型,或者其中一个是数字类型且另一个可以转换为数字类型(§5.1.8),则对操作数执行二进制数字提升(§5.6.2)。

显然,i可以转换为数字类型,而0是数字类型,因此对操作数执行二进制数字提升。

§ 5.6.2 二进制数值提升中说(除其他外):

如果操作数中有任何一个是引用类型,则执行拆箱转换(§5.1.8)。

§ 5.1.8 拆箱转换中说(除其他外):

如果 r 为 null,则拆箱转换会抛出 NullPointerException


0

只需编写一个方法并调用它即可避免NullPointerException。

public static Integer getNotNullIntValue(Integer value)
{
    if(value!=null)
    {
        return value;
    }
    return 0;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接