为什么要显式抛出NullPointerException而不是让它自然发生?


182

阅读JDK源代码时,笔者通常会检查参数是否为null,然后手动抛出新的NullPointerException()。他们为什么这样做?我认为没有必要这样做,因为它在调用任何方法时都会抛出新的NullPointerException()。(例如,以下是HashMap的一些源代码:)

public V computeIfPresent(K key,
                          BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    if (remappingFunction == null)
        throw new NullPointerException();
    Node<K,V> e; V oldValue;
    int hash = hash(key);
    if ((e = getNode(hash, key)) != null &&
        (oldValue = e.value) != null) {
        V v = remappingFunction.apply(key, oldValue);
        if (v != null) {
            e.value = v;
            afterNodeAccess(e);
            return v;
        }
        else
            removeNode(hash, key, null, false, true);
    }
    return null;
}

32
编码的重点是意图
可怕的袋熊

19
对于您的第一个问题,这是一个非常好的问题!我做了一些小的修改;我希望你不要介意。我还删除了谢谢和关于它的注释,这是您的第一个问题,因为通常这种事情不是SO问题的一部分。
大卫·康拉德

11
我是C#,惯例是ArgumentNullException在这种情况下提出(而不是NullReferenceException)-实际上,这是一个很好的问题,即为什么要在NullPointerException这里显式提出(而不是其他问题)。
EJoshuaS-恢复莫妮卡

21
@EJoshuaS 是否引发或是否为空参数是一个古老的争论。后者是JDK约定。IllegalArgumentExceptionNullPointerException
shmosel

33
真正的问题是,他们抛出错误并丢弃导致该错误的所有信息。看来这实际的源代码。甚至没有简单的流血字符串消息。伤心。
马丁·巴

Answers:


254

我想到许多原因,其中一些原因密切相关:

快速失败:如果要失败,最好早点而不是晚点。这样可以使问题更接近问题的根源,从而更易于识别和恢复。它还避免浪费CPU周期,而这些周期注定会失败。

目的:抛出异常可以使维护人员清楚地知道该错误是有目的的,并且作者意识到了后果。

一致性:如果允许错误自然发生,则可能不会在每种情况下都发生。例如,如果找不到映射,remappingFunction则将永远不会使用该映射,也不会引发异常。预先验证输入可提供更多确定性的行为和更清晰的文档

稳定性:代码会随着时间的推移而发展。自然地,遇到异常的代码可能会在经过一些重构之后停止这样做,或者在不同的情况下这样做。明确地扔掉它可以使行为无意更改的可能性降低。


14
同样:通过这种方式,引发异常的位置也正好与要检查的一个变量相关。没有它,该异常可能是由于多个变量之一为空。
詹斯·肖德

45
另一个:如果您等待NPE自然发生,那么中间的一些代码可能已经通过副作用改变了程序的状态。
托马斯

6
尽管此代码段没有执行此操作,但是您可以使用new NullPointerException(message)构造函数来说明什么是null。适用于无法访问您的源代码的人。他们甚至使用Objects.requireNonNull(object, message)实用程序方法在JDK 8中将其变成了单行代码。
罗宾(Robin)

3
故障应该在故障附近。“快速失败”不只是一个经验法则。您什么时候不想要这种行为?任何其他行为都意味着您正在隐藏错误。有“故障”和“故障”。失败是此程序消化NULL指针并崩溃时的结果。但是那行代码不是FAULT所在的地方。NULL来自某个地方-一个方法参数。谁通过了那个论点?从引用本地变量的代码行中。那是在哪里...看?糟透了。看到不良价值被存储应该是谁的责任?那时您的程序应该崩溃了。
诺亚·斯普里耶

4
@Thomas好点。Shmosel:Thomas的观点可能暗示为快速失败点,但这有点隐瞒。这是一个足够重要的概念,它有自己的名称:失败原子性。请参阅Bloch,有效的Java,项目46。它具有比快速失败更强的语义。我建议在另一个地方讲出来。顺便说一句,总体而言,答案很好。+1
斯图尔特(Stuart Marks)

40

这是为了清楚,一致,并防止执行额外的不必要的工作。

考虑如果方法顶部没有保护子句,将会发生什么情况。它总是会调用hash(key)getNode(hash, key)甚至在NPE抛出之前null已传递给它时remappingFunction也是如此。

更糟糕的是,如果if条件是条件,false那么我们采用else完全不使用的分支remappingFunction,这意味着当出现以下情况时,该方法并不总是抛出NPEnull转到传递。是否确实取决于地图的状态。

两种情况都是不好的。如果null不是的有效值remappingFunction该方法应始终引发异常,而不管调用时对象的内部状态如何,并且应该这样做,而不必执行不必要的工作,因为它将要抛出该异常。最后,干净,清晰的代码的一个很好的原则是使保护措施处于正确的位置,以便任何审阅源代码的人都可以很容易地看出来。

即使当前每个代码分支都抛出异常,将来的代码修订版也可能会对此进行更改。从一开始就执行检查,可以确保一定会进行检查。


25

除了@shmosel的出色答案列出的原因...

性能:(在某些JVM上)(明确地)抛出NPE(而不是让JVM执行)可能(已经)具有性能优势。

这取决于Java解释器和JIT编译器采取的检测空指针取消引用的策略。一种策略是不测试是否为空,而是捕获当指令尝试访问地址0时发生的SIGSEGV。在引用始终有效的情况下,这是最快的方法,但在NPE情况下,这是昂贵的。

null在NPE频繁出现的情况下,对代码中的进行显式测试将避免SIGSEGV性能受到影响。

(我怀疑这在现代JVM中是否值得进行微优化,但它可能已经过去了。)


兼容性:异常中没有消息的可能原因是与JVM本身抛出的NPE兼容。在兼容的Java实现中,JVM抛出的NPE有一条null消息。(Android Java不同。)


20

除了其他人指出的内容外,值得注意的是约定在这里的作用。例如,在C#中,您还具有在此类情况下显式引发异常的相同约定,但是它专门是一个ArgumentNullException,它更加具体。(C#约定NullReferenceException 始终代表某种错误-很简单,它永远不会在生产代码中发生;当然,ArgumentNullException通常也是如此,但是它可能更多是“您不愿意”的错误。了解如何正确使用该库”。

因此,基本上,在C#中NullReferenceException意味着您的程序实际上尝试使用它,而ArgumentNullException这意味着它认识到该值是错误的,甚至没有费心尝试使用它。实际上,含义可能有所不同(取决于情况),因为这ArgumentNullException意味着所讨论的方法还没有副作用(因为它没有满足方法的先决条件)。

顺便说一句,如果您提出类似ArgumentNullException或的问题IllegalArgumentException,那是进行检查的一部分:您想要的异常不同于“正常”得到的异常。

无论哪种方式,显式地引发异常都可以加强一种良好的做法,即明确说明方法的前提条件和期望的参数,从而使代码更易于阅读,使用和维护。如果您未明确检查null,我不知道是否是因为您认为没有人会传递一个null参数,而是在计算它是否会引发异常,或者只是忘记检查该参数。


4
+1为中间段落。我认为所讨论的代码应该“引发新的IllegalArgumentException(“ remappingFunction不能为null”);' 这样就可以立即清楚出了什么问题。所示的NPE有点含糊。
克里斯·帕克

1
@ChrisParker我曾经有过相同的看法,但事实证明,NullPointerException旨在表示传递给期望非空参数的方法的空参数,除了是对取消引用空值的尝试的运行时响应之外。来自javadoc:“应用程序应抛出此类的实例以指示该null对象的其他非法使用。” 我对此并不感到疯狂,但这似乎是预期的设计。
VGR

1
我同意@ChrisParker-我认为该异常更为具体(因为该代码从未尝试使用该值做任何事情,它立即意识到不应使用该值)。在这种情况下,我喜欢C#约定。C#约定是NullReferenceException(等同于NullPointerException)表示您的代码实际上尝试使用它(这始终是一个错误-它永远不会在生产代码中发生),而“我知道参数是错误的,所以我什至没有尝试使用它。” 还有ArgumentException(这意味着该论证由于其他原因是错误的)。
EJoshuaS-恢复莫妮卡

2
我会这么说,我总是如所述抛出IllegalArgumentException。当我觉得惯例很愚蠢时,我总是觉得自己很无礼。
克里斯·帕克

1
@PieterGeerkens-是的,因为NullPointerException第35行比IllegalArgumentException(“ Function不能为null”)第35行好得多。
克里斯·帕克

12

这样一来,您将在执行错误后立即获得异常,而不是稍后在使用地图且不了解发生原因时。


9

它将看似不稳定的错误情况转变为明显的违反合同的情况:该函数具有正常工作的一些先决条件,因此它会预先检查它们,以使其得到满足。

结果是,computeIfPresent()当您摆脱异常时,您不必调试。一旦看到异常来自先决条件检查,便知道您使用非法参数调用了该函数。如果检查不存在,则需要排除computeIfPresent()自身内部存在某些导致引发异常的错误的可能性。

显然,抛出泛型NullPointerException确实是一个糟糕的选择,因为它本身并不表示违反合同。IllegalArgumentException会是一个更好的选择。


旁注:
我不知道Java是否允许这样做(我对此表示怀疑),但是C / C ++程序员assert()在这种情况下会使用,这对于调试而言要好得多:它告诉程序立即崩溃,并在崩溃时尽可能地崩溃。条件评估为假。所以,如果你跑了

void MyClass_foo(MyClass* me, int (*someFunction)(int)) {
    assert(me);
    assert(someFunction);

    ...
}

在调试器中,并将任何东西传递NULL给任一自变量,程序将立即在告诉该自变量为的行处停止NULL,您将可以随意检查整个调用堆栈的所有局部变量。


1
assert something != null;但这-assertions在运行应用程序时需要标志。如果该-assertions标志不存在,则assert关键字将不会引发AssertionException
Zoe

我同意,这就是为什么我在这里更喜欢C#约定-空引用,无效参数和空参数通常都暗示某种错误,但是它们暗示着不同类型的错误。“您正在尝试使用空引用”与“您正在滥用库”通常有很大的不同。
EJoshuaS-恢复莫妮卡

7

这是因为它是可能它不是自然发生的。让我们看一下这样的代码:

bool isUserAMoron(User user) {
    Connection c = UnstableDatabase.getConnection();
    if (user.name == "Moron") { 
      // In this case we don't need to connect to DB
      return true;
    } else {
      return c.makeMoronishCheck(user.id);
    }
}

(当然,此示例中有很多关于代码质量的问题。很抱歉懒得想象完美的示例)

情况时,c将不被实际使用,并NullPointerException即使不被拆毁c == null是可能的。

在更复杂的情况下,查找此类情况变得非常不容易。这就是为什么一般检查if (c == null) throw new NullPointerException()更好。


可以说,一段不需要实际连接而无需数据库连接的代码是一件好事,而连接到数据库以查看是否可以失败的代码通常很烦人。
德米特里·格里戈里耶夫

5

有意保护进一步的损坏或进入不一致状态。


1

除了这里所有其他出色的答案之外,我还想补充一些案例。

如果您创建自己的异常,则可以添加一条消息

如果您自己丢东西NullPointerException,则可以添加一条消息(绝对应该!)

默认消息是nullfrom new NullPointerException()和使用它的所有方法,例如Objects.requireNonNull。如果您打印该null,它甚至可以转换为空字符串。

有点短而且没有信息...

堆栈跟踪将提供很多信息,但是要使用户知道什么为空,他们必须挖掘代码并查看确切的行。

现在想象一下,NPE是通过网络包装和发送的,例如作为Web服务错误中的一条消息,可能在不同部门甚至组织之间。最坏的情况是,没人会知道null代表什么。

链接方法调用将使您不断猜测

异常只会告诉您异常发生在哪一行。考虑以下行:

repository.getService(someObject.someMethod());

如果你得到一个NPE和它指向该行,哪一个repositorysomeObject为空?

相反,在获取这些变量时检查它们至少会指向一行,希望它们是唯一要处理的变量。并且,如前所述,如果您的错误消息包含变量名称或类似名称,则更好。

处理大量输入时的错误应提供识别信息

想象一下,您的程序正在处理具有数千行的输入文件,并且突然出现NullPointerException。您看了一下地方,发现有些输入不正确...什么输入?您将需要有关行号的更多信息,也许是列甚至整个行的文本,以了解该文件中的哪行需要修复。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.