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

Posted

技术标签:

【中文标题】为啥要显式抛出 NullPointerException 而不是让它自然发生?【英文标题】:Why explicitly throw a NullPointerException rather than letting it happen naturally?为什么要显式抛出 NullPointerException 而不是让它自然发生? 【发布时间】:2017-10-11 05:34:03 【问题描述】:

在阅读JDK源码时,发现作者经常会检查参数是否为null,然后手动抛出new NullPointerException()。 他们为什么这样做?我认为没有必要这样做,因为它会在调用任何方法时抛出 new 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;

【问题讨论】:

编码的一个关键点是意图 这是你第一个问题的一个很好的问题!我做了一些小的修改;我希望你不要介意。我还删除了谢谢和关于它是你的第一个问题的注释,因为通常这类事情不是 SO 问题的一部分。 我是 C#,惯例是在这种情况下提出 ArgumentNullException(而不是 NullReferenceException)——这实际上是一个非常好的问题,为什么要明确提出 NullPointerException在这里(而不是另一个)。 @EJoshuaS 这是一个old debate 是否抛出IllegalArgumentExceptionNullPointerException 作为空参数。 JDK约定是后者。 真正的问题是他们抛出一个错误和throw away all information that led to this error。似乎这是IS the actual 源代码。甚至连简单的血腥字符串消息都没有。伤心。 【参考方案1】:

这是为了保护进一步的损坏,或进入不一致的状态。

【讨论】:

【参考方案2】:

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

考虑如果方法顶部没有保护子句会发生什么。它总是会调用hash(key)getNode(hash, key),即使在抛出NPE 之前已经为remappingFunction 传递了null

更糟糕的是,如果if 条件是false,那么我们将采用else 分支,它根本不使用remappingFunction,这意味着当@ 时该方法并不总是抛出NPE 987654329@通过;是否确实取决于地图的状态。

这两种情况都很糟糕。如果null 不是remappingFunction 的有效值,则无论调用时对象的内部状态如何,该方法都应该始终抛出异常,并且它应该这样做而不做不必要的工作,因为它没有意义只是要扔。最后,干净、清晰的代码是一个很好的原则,在前面有保护,这样任何审查源代码的人都可以很容易地看到它会这样做。

即使当前每个代码分支都抛出了异常,未来的代码版本也可能会改变这一点。一开始就执行检查确保它一定会执行。

【讨论】:

【参考方案3】:

这样一来,您就会在犯错时立即收到异常,而不是稍后在您使用地图并且不明白为什么会发生时。

【讨论】:

【参考方案4】:

我想到了很多原因,其中几个密切相关:

快速失败:如果要失败,最好尽早失败。这可以让问题更接近其根源,从而更容易识别和恢复。它还避免了在必然会失败的代码上浪费 CPU 周期。

意图:明确地抛出异常可以让维护者清楚地知道错误是故意存在的,并且作者已经意识到了后果。

一致性:如果允许错误自然发生,它可能不会在所有情况下都发生。例如,如果未找到映射,则永远不会使用 remappingFunction 并且不会引发异常。提前验证输入允许更确定的行为和更清晰的documentation。

稳定性:代码会随着时间而发展。遇到异常的代码自然可能会在一些重构之后停止这样做,或者在不同的情况下这样做。明确地抛出它可以减少无意中改变行为的可能性。

【讨论】:

另外:这样,抛出异常的位置就与一个被检查的变量相关联。没有它,异常可能是由于多个变量之一为空。 另一个:如果你等待 NPE 自然发生,一些中间代码可能已经通过副作用改变了你的程序状态。 虽然这个 sn-p 不这样做,但您可以使用 new NullPointerException(message) 构造函数来阐明什么是 null。适合无法访问您的源代码的人。他们甚至使用 Objects.requireNonNull(object, message) 实用方法在 JDK 8 中将其作为单行代码。 FAILURE 应该在 FAULT 附近。 “快速失败”不仅仅是一个经验法则。你什么时候不想要这种行为?任何其他行为都意味着您在隐藏错误。有“故障”和“故障”。 FAILURE 是当这个程序消化一个 NULL 指针并崩溃时。但是那行代码不是 FAULT 所在的地方。 NULL 来自某个地方——一个方法参数。谁通过了这个论点?从引用局部变量的某行代码。那是在哪里……看到了吗?太糟糕了。看到坏值被存储应该是谁的责任?你的程序应该已经崩溃了。 @Thomas 好点。 Shmosel:Thomas 的观点可能暗示在快速失败点中,但它有点被埋没了。这是一个足够重要的概念,它有自己的名字:failure atomicity。参见 Bloch,Effective Java,Item 46。它比 fail-fast 具有更强的语义。我建议单独提出来。总的来说,答案很好,顺便说一句。 +1【参考方案5】:

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

性能: 显式抛出 NPE 而不是让 JVM 去​​做可能会/已经获得性能优势(在某些 JVM 上)。

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

在代码中对null 进行显式测试可以避免在 NPE 频繁出现的情况下对 SIGSEGV 性能造成影响。

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


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

【讨论】:

【参考方案6】:

除了其他人指出的,值得注意的是约定在这里的作用。例如,在 C# 中,您也有在这种情况下显式引发异常的相同约定,但它特别是 ArgumentNullException,它更具体一些。 (C# 约定是 NullReferenceException 总是 代表某种错误 - 很简单,它不应该永远 发生在生产代码中;当然,ArgumentNullException 通常也有,但它可能更像是“你不了解如何正确使用库”之类的错误。

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

顺便说一句,如果您提出 ArgumentNullExceptionIllegalArgumentException 之类的问题,这就是进行检查的重点之一:您想要一个不同于“通常”得到的异常。

无论哪种方式,显式引发异常都强化了明确说明方法的前置条件和预期参数的良好做法,从而使代码更易于阅读、使用和维护。如果您没有明确检查null,我不知道是不是因为您认为没有人会传递null 参数,您还是在算它抛出异常,或者您只是忘记了检查一下。

【讨论】:

+1 表示中间段落。我认为有问题的代码应该'throw new IllegalArgumentException("remappingFunction cannot be null");'这样就可以立即看出哪里出了问题。显示的 NPE 有点模棱两可。 @ChrisParker 我曾经持有相同的观点,但事实证明 NullPointerException 除了作为运行时响应之外,还旨在表示传递给期望非空参数的方法的空参数试图取消对 null 的引用。来自 javadoc:“应用程序应抛出此类的实例以指示 null 对象的其他非法用途。”我对此并不感到疯狂,但这似乎是预期的设计。 我同意,@ChrisParker - 我认为该异常更具体(因为代码甚至从未尝试对值做任何事情,它立即意识到它不应该使用它)。在这种情况下,我喜欢 C# 约定。 C# 约定是NullReferenceException(相当于NullPointerException)意味着您的代码实际上试图使用它(这始终是一个错误——它不应该在生产代码中发生),而不是“我知道这个论点是错误的,所以我什至没有尝试使用它。”还有ArgumentException(这意味着该论点由于某些其他原因是错误的)。 我会说这么多,我总是按照描述抛出一个 IllegalArgumentException。当我觉得惯例很愚蠢时,我总是很自在地蔑视惯例。 @PieterGeerkens - 是的,因为 NullPointerException 第 35 行比 IllegalArgumentException("Function can't be null") 第 35 行好得多。真的吗?【参考方案7】:

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

效果是,当您从中获取异常时,您不必调试computeIfPresent()。一旦您看到异常来自前置条件检查,您就知道您使用非法参数调用了该函数。如果检查不存在,则需要排除 computeIfPresent() 本身存在导致引发异常的错误的可能性。

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


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

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

    ...

在调试器下,将NULL 传递给任一参数,程序将停在告诉哪个参数是NULL 的那一行,您将能够在闲暇时检查整个调用堆栈的所有局部变量.

【讨论】:

assert something != null; 但在运行应用程序时需要-assertions 标志。如果-assertions 标志不存在,则 assert 关键字不会抛出 AssertionException 我同意,这就是为什么我更喜欢这里的 C# 约定 - 空引用、无效参数和空参数通常都暗示某种错误,但它们暗示不同的种类的错误。 “您正在尝试使用空引用”通常与“您在滥用库”非常不同。【参考方案8】:

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

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() 这样的一般检查更好。

【讨论】:

可以说,一段代码在不需要数据库连接的情况下工作是一件好事,而连接到数据库只是为了看看它是否会失败的代码通常是很烦人。【参考方案9】:

除了这里所有其他优秀的答案,我还想补充几个案例。

如果您创建自己的例外,则可以添加消息

如果您抛出自己的NullPointerException,您可以添加一条消息(您绝对应该这样做!)

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

有点短,信息量不大……

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

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

链式方法调用会让您不断猜测

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

repository.getService(someObject.someMethod());

如果你得到一个 NPE 并且它指向这一行,repositorysomeObject 中的哪一个是空的?

相反,当你得到这些变量时检查它们至少会指向一行,希望它们是唯一被处理的变量。而且,如前所述,如果您的错误消息包含变量名称或类似名称,那就更好了。

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

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

【讨论】:

以上是关于为啥要显式抛出 NullPointerException 而不是让它自然发生?的主要内容,如果未能解决你的问题,请参考以下文章

为啥要显式调用 asyncio.StreamWriter.drain?

JVM的异常处理

Jvm(49),指令集----异常处理指令

关于异常处理的几点建议

还搞不清楚JVM是怎么处理异常的?这水平,跳槽都没人要

为什么要显式调用asyncio.StreamWriter.drain?