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

LiJ*_*ing 182 java nullpointerexception

在阅读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;
}
Run Code Online (Sandbox Code Playgroud)

shm*_*sel 252

有几个原因浮现在脑海中,有几个原因密切相关:

快速失败:如果它失败了,最好早点而不是晚点失败.这样可以使问题更接近其来源,从而更容易识别和恢复.它还可以避免在必然会失败的代码上浪费CPU周期.

意图:明确地抛出异常使维护者清楚地知道错误是故意的,并且作者意识到了后果.

一致性:如果允许错误自然发生,则可能不会在每个方案中发生.例如,如果没有找到映射,则remappingFunction永远不会使用,并且不会抛出异常.事先验证输入允许更确定的行为和更清晰的文档.

稳定性:代码随着时间的推移而发展 在经过一些重构后,遇到异常的代码可能会停止这样做,或者在不同情况下这样做.明确地抛出它会使行为不太可能无意中发生变化.

  • 另一个:如果你等待 NPE 自然发生,一些中间代码可能已经通过副作用改变了你的程序状态。 (45认同)
  • 另外:这样抛出异常的位置就与一个被检查的变量紧密相关.没有它,异常可能是由于多个变量之一为空. (14认同)
  • 虽然这个代码片段没有这样做,但您可以使用`new NullPointerException(message)`构造函数来阐明什么是null.适合无法访问源代码的人.他们甚至使用`Objects.requireNonNull(object,message)`实用程序方法在JDK 8中创建了这个单线程. (6认同)
  • @Thomas好点.Shmosel:托马斯的观点可能隐含在失败的快点中,但却被埋没了.这是一个非常重要的概念,它有自己的名字:**失败原子性**.参见Bloch,*Effective Java*,Item 46.它具有比fail-fast更强的语义.我建议在另外一点上说出来.总的来说,答案非常好.+1 (4认同)
  • FAILURE应该在FAULT附近."快速失败"不仅仅是经验法则.你什么时候不想要这种行为?任何其他行为都意味着您隐藏了错误.有"FAULTS"和"FAILURES".FAILURE是这个程序消化NULL指针并崩溃的时候.但是那行代码并不是FAULT的所在.NULL来自某个地方 - 一个方法参数.谁通过了那个论点?从引用局部变量的某些代码行.那是哪里......看到了吗?太糟糕了.看到一个坏的价值被存储应该是谁的责任?你的程序应该崩溃了. (3认同)

Dav*_*rad 39

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

考虑如果方法顶部没有保护条款会发生什么.它会一直打电话hash(key),getNode(hash, key)甚至在NPE被抛出之前null被传入remappingFunction.

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

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

即使代码的每个分支当前都抛出异常,代码的未来版本也可能会改变它.在开头执行检查确保它肯定会被执行.


Ste*_*n C 25

除了@ shmosel的优秀答案所列的原因......

性能:明确抛出NPE而不是让JVM这样做,可能会有(在某些JVM上)性能优势.

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

null在代码中进行显式测试可以避免在NPE频繁出现的情况下出现SIGSEGV性能损失.

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


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


EJo*_*ica 20

除了其他人指出的内容外,值得注意的是会议的作用.例如,在C#中,您也有相同的约定,即在这种情况下显式引发异常,但它具体是一个ArgumentNullException,它更具体一些.(C#的惯例是,NullReferenceException 始终代表了某种错误-很简单,它不应该永远发生在生产代码;授予,ArgumentNullException一般也有同样效果,但它可能是一个错误大致为"你不这样做行更了解如何正确使用库"一种bug".

因此,基本上,在C#中NullReferenceException意味着您的程序实际上试图使用它,而ArgumentNullException这意味着它认识到该值是错误的并且它甚至都不愿意尝试使用它.影响实际上可能是不同的(取决于具体情况),因为这ArgumentNullException意味着所讨论的方法还没有副作用(因为它失败了方法的前提条件).

顺便说一句,如果你提出类似ArgumentNullException或者IllegalArgumentException,这就是做检查点的一部分:你想有一个不同的异常比你"正常"获得.

无论哪种方式,明确提出异常都强化了明确说明方法的前置条件和预期参数的良好做法,这使得代码更易于阅读,使用和维护.如果你没有明确地检查null,我不知道是不是因为你认为没有人会传递一个null参数,你还在计算它抛出异常,或者你只是忘了检查它.

  • 中间段+1.我认为有问题的代码应该'抛出新的IllegalArgumentException("remappingFunction不能为null");' 这样就可以立即明白出现了什么问题.显示的NPE有点含糊不清. (4认同)
  • 我会说这么多,我总是按照描述抛出IllegalArgumentException.当我觉得惯例是愚蠢的时候,我总是觉得很舒服. (2认同)

use*_*421 12

因此,一旦您执行错误,您将立即获得异常,而不是在您使用地图时以及将无法理解为什么会发生错误.


cma*_*ter 9

它将看似不稳定的错误条件转变为明确的合同违规:该函数具有正常工作的一些先决条件,因此它会事先检查它们,强制执行它们.

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

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


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

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

    ...
}
Run Code Online (Sandbox Code Playgroud)

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


Are*_*nim 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

(当然这个样本中存在很多关于代码质量的问题.抱歉懒得想象完美的样本)

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

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


Fai*_*roz 5

故意保护进一步的损害,或进入不一致的状态.