如果equals(null)抛出NullPointerException,这是一个不好的想法吗?

77

equals方法在处理null时的契约如下:

对于任何非null引用值 xx.equals(null) 应该返回false

这是相当奇怪的,因为如果 o1 != nullo2 == null,那么我们有:

o1.equals(o2) // returns false
o2.equals(o1) // throws NullPointerException

"

o2.equals(o1)抛出NullPointerException是一件好事,因为它提醒我们程序员的错误。然而,如果由于各种原因我们只是将其切换到o1.equals(o2),那么这个错误就不会被捕获,而只会“静默失败”。

所以问题是:

  • 为什么o1.equals(o2)应该返回false而不是抛出NullPointerException
  • 如果可能的话,我们是否应该重写合同,使得anyObject.equals(null)总是抛出NullPointerException
"

Comparable的比较

相比之下,这是Comparable合同所说:

请注意,null不是任何类的实例,e.compareTo(null)应抛出NullPointerException,即使e.equals(null)返回false

如果对于compareTo来说,NullPointerException是合适的,那么为什么对于equals不是呢?

相关问题


一个纯语义的论点

这是Object.equals(Object obj)文档中的实际词语:

指示是否有一些其他对象与此对象“相等”。

那么什么是对象?

JLS 4.3.1 对象

对象是一个类实例或数组。

引用值(通常只是引用)是指向这些对象的指针,有一个特殊的null引用,它没有任何对象。

从这个角度来说,我的论点非常简单。

  • equals测试是否有一些其他对象this“相等”
  • null引用没有为测试提供其他对象
  • 因此,equals(null)应该抛出NullPointerException

2
在这里评论,事实已经证明,在Java中,由于equals()位于OO层次结构的最顶部,除了最简单的情况(即您根本没有进行面向对象编程时),不可能尊重等式契约。认为存在一种非损坏的Java equals()契约是妄想的。我们更进一步:默认情况下,equals()hashCode()会抛出UOE。如果您想使用这些方法,则必须记录您如何处理此处提出的基本问题:http://www.artima.com/lejava/articles/equality.html - SyntaxT3rr0r
1
我的问题与equals的不可争议的破碎性相关,在这里获得了8个投票和3个收藏:https://dev59.com/V3E95IYBdhLWcg3wrP6w。事实是:“常见的等式智慧”根本行不通。不仅像Joshua Bloch和Martin Odersky这样的人在说,而且你可以使用逻辑来证明这一事实。你不能简单地进行OOA/OOD到OOP的转换,并希望重用Java的相等概念:对我来说,这是语言中的一个根本缺陷,即equals存在于Object中。当然,那些喝Gosling kool-aid的人会不同意。让他们与Bloch争论吧。 - SyntaxT3rr0r
1
我的最终观点是:在许多情况下,问题不在于抛出NPE或返回false,而在于抛出一个巨大的UnsupportedOperationException。Java允许在本质上不应该具有相等概念的对象上调用equals,这是Java的缺陷。著名的最后一句话:UnsupportedOperationException :) - SyntaxT3rr0r
12个回答

108

针对这个不对称性是否不一致的问题,我认为不是,并引用这个古老的禅宗公案:

  • 问任何人他是否和下一个人一样好,每个人都会回答“是”。
  • 问任何人他是否和没有人一样好,每个人都会回答“否”。
  • 问没有人是否和任何人一样好,你永远得不到回复。

就在那一瞬间,编译器达到了启蒙境界。


7
我想知道Java编程语言的设计者是否真正想到了这一点,还是只是没有注意到这种不对称性...;-) - Jesper
1
Koan 不是拼写为 ko-an,显然字母 o 上面有一个长音符号。 - Digital Impermanence
1
好的,我会修复它,因为这几乎是我最受欢迎的答案。一定要弄对。 - Sean Owen

19

异常真的应该是一种异常情况。空指针可能不是程序员错误。

你引用了现有的合同。如果你决定违反惯例,当每个Java开发人员都期望equals返回false时,你会做出意外和不受欢迎的事情,从而使你的类成为被避开的对象。

我完全不同意。如果我是它的客户端,我不会经常重写equals来抛出异常,我会替换任何这样做的类。


我喜欢这个问题的推理(如果变量为“null”,则无法使用它) - 但是@duffymo是正确的,他的答案得到了Java代码本身如何实现许多类的.equals()的支持(请在源代码中检查)。相等性首先使用==进行检查,然后使用instanceOf进行不等式检查。根据定义,这两个检查都将返回一个布尔值,而不会出现NullPointerException - 即在证明给定对象为非“null”之前,该对象不会在.equals()方法中使用。因此,尽可能编写自己的类以遵循相同的方式。 - PMorganCA

8

想一下 .equals 和 == 的关系,以及 .compareTo 与比较运算符 >, <, >=, <= 的关系。

如果你主张使用 .equals 将一个对象与 null 进行比较应该抛出 NPE,那么你也必须主张这段代码也应该抛出 NPE:

Object o1 = new Object();
Object o2 = null;
boolean b = (o1 == o2); // should throw NPE here!

o1.equals(o2)和o2.equals(o1)的区别在于,在第一种情况下,您正在将某些内容与null进行比较,类似于o1 == o2,而在第二种情况下,equals方法实际上根本没有被执行,因此根本没有进行任何比较。

关于.compareTo协议,将非空对象与空对象进行比较就像尝试执行以下操作:

int j = 0;
if(j > null) { 
   ... 
}

显然,这段代码无法编译。您可以使用自动拆箱使其编译,但在进行比较时会出现NPE,这与.compareTo协定一致:

Integer i = null;
int j = 0;
if(j > i) { // NPE
   ... 
}

我认为在涉及到对象时,等号(==)和关系运算符(<、>、<=、>=)之间存在着根本的差别。 关系对象不能将对象作为操作数(即使它们是相同类型的),因此必须使用compareTo方法。 另一方面,equals方法并不是等号运算符的替代品。 等号运算符仍然可以用于对象上(假设它们是同一类型),并且与equals方法具有不同的目的。 - Shazz

4

虽然这并不一定是回答你问题的答案,但它是一个例子,说明我觉得现在的行为方式很有用。

private static final String CONSTANT_STRING = "Some value";
String text = getText();  // Whatever getText() might be, possibly returning null.

目前我能做到这一点。
if (CONSTANT_STRING.equals(text)) {
    // do something.
}

我不可能遇到NullPointerException异常。如果按照你的建议更改,我将不得不执行以下操作:

if (text != null && text.equals(CONSTANT_STRING)) {
    // do something.
}

这个原因是否足以解释为什么行为是这样的?我不知道,但这是一个有用的副作用。


4
如果考虑面向对象的概念,并考虑整个发送器和接收器角色,我认为行为很方便。在第一种情况下,您正在询问一个对象是否等于无人。他应该说“不,我不是”。但是,在第二种情况下,您没有任何人的参考。所以你实际上并没有问任何人。这应该抛出异常,而第一个案例不应该。
我认为只有在您忘记面向对象并将表达式视为数学相等性时,才会出现不对称。然而,在这种范例中,两端扮演不同的角色,因此可以预期顺序很重要。
最后一点。当您的代码出现错误时,应引发空指针异常。但是,询问一个对象是否是没有人不应被视为编程缺陷。我认为问一个对象是否不为空是完全可以的。如果您不能控制提供对象的来源,并且此来源向您发送null,则会发生什么?您会检查对象是否为空,然后再看它们是否相等吗?将两者进行比较,无论第二个对象是什么,比较都将在没有异常的情况下进行,这更直观。
老实说,如果equals方法在其内部故意返回空指针异常,我会很恼火。Equals旨在用于任何类型的对象,因此不应对其接收的内容要求太严格。如果equals方法返回npe,我最后会想到的是它故意这样做。特别是考虑到它是未检查异常。如果您提出了npe,那个人必须记得在调用您的方法之前始终检查null,或者更糟糕,围绕对equals的调用使用try / catch块(天哪,我讨厌try / catch块)。但无论如何...

1
“你是否等于无人”这个问题有些含糊不清。另一方面,equals方法并没有给出一个对象——它给出了一个对象的引用。实际上,这个问题应该是“这个引用是否标识了一个与你相等的对象”。这样的问题很容易回答:“不,因为这个引用并没有标识任何对象,当然也就不会标识一个等同于我的对象了”。 - supercat
请注意,重新制定的问题可以证明涉及任何对象类型的相等比较。如果有人被问到“你是否与红色相等”,可能会认为这个问题是格式错误的,但是“这个引用(它标识了红色)是否指向一个与你等价的对象”就不是了。该引用标识的东西与人不等价,因此答案是“否”。 - supercat
我理解你所做的区别,并且我同意。然而,在实践中,你用更准确和技术性的术语说了我所说的话。如果你从我的回答中得到了相反的想法,那么也许是我表达不清楚。 - arg20
你表达得很好;我的意思是建议对问题进行明确的重新表述,即使出现空引用也不会有任何问题。很遗憾,在一对对象上执行“equals”检查并且其中一个或两个都可能为空时,没有简洁的符号表示法。 - supercat
顺便说一下,我希望在现代面向对象语言中能够声明性地或者语法上区分引用封装对象的标识的情况,封装的各种情况(具有区分共享与非共享、可变与只读与不可变、安全可变与不可变等不同组合的能力),以及偶尔需要引用本身成为一个实体的情况。Java 在这方面非常薄弱,不幸的是,.NET 借鉴了它的很多缺点。 - supercat

2

就我个人而言,我更希望它的表现如现在这样。

NullPointerException 表明问题出在执行 equals 操作的对象上。

如果按照您的建议使用 NullPointerException,并尝试执行(有点无意义的)操作...

o1.equals(o1) 其中 o1 = null... 那么 NullPointerException 是因为您的比较函数出了问题还是因为 o1 是 null 但您没有意识到呢? 虽然这是一个极端的例子,但是根据当前的行为,我认为您可以轻松地知道问题所在。


2
在第一种情况下,o1.equals(o2)返回false,因为o1不等于o2,这是完全可以接受的。在第二种情况下,会抛出NullPointerException,因为o2null。不能在null上调用任何方法。虽然这可能是编程语言的一种限制,但我们必须适应它。
此外,抛出NullPointerException也不是一个好主意,因为你违反了equals方法的契约并且使事情变得更加复杂化,这是没有必要的。

2
有许多常见情况下,null并不是异常情况,例如,它可能只是表示一个键没有值的(非异常)情况,或者代表“nothing”。因此,对于未知的y执行x.equals(y)也很常见,而且必须始终先检查null将会浪费时间。
至于为什么null.equals(y)不同,这是Java中在空引用上调用任何实例方法都是编程错误,因此值得引发异常。应选择xyx.equals(y)中的顺序,以使已知x不是null。我认为,在几乎所有情况下,可以根据先前了解的对象(例如,从它们的起源或通过检查其他方法调用的null)来进行重新排序。
同时,如果两个对象的“nullness”都未知,则其他代码几乎肯定需要检查它们中的至少一个,否则在不冒NullPointerException的风险的情况下无法对该对象进行操作。
由于这是指定的方式,因此破坏合同并为equalsnull参数引发异常是编程错误。如果考虑要求抛出异常的替代方案,则每个equals的实现都必须进行特殊处理,并且每次调用任何可能为null对象的equals时都必须在调用之前检查。
它可以被指定为不同的方式(即,equals的前提条件要求参数为非-null),因此这并不是说您的论证无效,但当前的规范使得编程语言更简单、更实用。

你实际上可以在空引用上调用静态方法,所以你的答案并不完全准确。不过应该毫无疑问地说,这是一件非常糟糕的事情。 - CurtainDog

1
请注意合同是“对于任何非空引用x”。因此,实现将如下所示:
if (x != null) {
    if (x.equals(null)) {
        return false;
    }
}

x不需要是null就可以被视为等于null,因为以下equals的定义是可行的:

public boolean equals(Object obj) {
    // ...
    // If someMember is 0 this object is considered as equal to null.
    if (this.someMember == 0 and obj == null) {
         return true;
    }
    return false;
}

1

我认为这是关于方便性和更重要的一致性 - 允许 null 成为比较的一部分避免了每次调用 equals 时都要进行 null 检查并实现其语义。在许多集合类型中,null 引用是合法的,因此它们可以出现在比较的右侧是有意义的。

使用实例方法来进行相等性、比较等操作,必然会使排列不对称 - 这是多态性带来的巨大收益所付出的一点小麻烦。当我不需要多态性时,有时我会创建一个对称的静态方法,带有两个参数,MyObject.equals(MyObjecta, MyObject b)。然后该方法检查一个或两个参数是否为 null 引用。如果我特别想排除 null 引用,那么我会创建一个额外的方法,例如 equalsStrict() 或类似的方法,在委托给其他方法之前进行 null 检查。


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