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

在阅读JDK源代码时,我发现作者常常会检查参数是否为空,然后手动抛出新的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; } 

有几个原因可以想到,有几个是密切相关的:

快速失败:如果失败,最好早日失败。 这样可以使问题更接近他们的来源,使他们更容易识别和恢复。 它还避免了在必然失败的代码上浪费CPU周期。

意图:抛出exception明确地向维护者清楚地表明错误在那里是故意的,作者意识到了后果。

一致性:如果允许错误自然发生,则可能不会在每种情况下发生。 如果没有find映射,例如, remappingFunction将永远不会被使用,并且不会抛出exception。 事先validationinput允许更确定的行为和更清晰的文档 。

稳定性:代码随着时间而演变。 自然遇到exception的代码可能会在一些重构之后停止,或者在不同情况下这样做。 明确投掷使得不太可能不经意地改变行为。

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

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

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

两种情况都不好。 如果null对于remappingFunction来说不是一个有效的值,那么不pipe对象在调用时的内部状态如何,该方法都应该持续地抛出一个exception,并且应该这样做,而不会做不必要的工作,这是毫无意义的,因为它只是要扔。 最后,清晰,清晰的代码是一个很好的原则,让任何人查看源代码都可以很容易地看到它会这样做。

即使目前每个代码分支都抛出exception,未来版本的代码也可能会改变这种情况。 在开始时执行检查确保它一定会被执行。

除了@ shmosel的优秀答案列出的原因…

性能:在某些JVM上可能会有明显的性能优势,而不是让JVM执行它。

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

在代码中显式testingnull会避免在NPE频繁的情况下SIGSEGV性能的下降。

(我怀疑这在现代JVM中是否是一个有价值的微型优化,但是可能是在过去。)


兼容性:exception中没有消息的可能原因是为了与JVM自身引发的NPE兼容。 在一个兼容的Java实现中,由JVM抛出的NPE有一个null消息。 (Android的Java是不同的。)

除了其他人所指出的,这里值得注意的是会议的作用。 在C#中,例如,在这种情况下,您也有相同的约定来显式引发exception,但是它特别是一个ArgumentNullException ,这个更具体一些。 (C#约定是NullReferenceException 总是代表某种错误 – 很简单,它不应该在生产代码中发生;被授予, ArgumentNullException通常也是这样,但它可能是一个错误,不明白如何正确使用库“的一种错误)。

所以,基本上,在C#中的NullReferenceException意味着你的程序实际上试图使用它,而ArgumentNullException它意味着它认识到这个值是错误的,它甚至懒得试图使用它。 其含义可能实际上是不同的(取决于具体情况),因为ArgumentNullException意味着所讨论的方法没有副作用(因为它失败了方法前置条件)。

顺便提一句,如果你提出了像ArgumentNullExceptionIllegalArgumentException这样的事情,这是做检查点的一部分:你想要一个不同的exception比你“正常”得到。

无论哪种方式,明确提出exception都会强化对方法的前提条件和预期的参数进行明确的良好实践,这使代码更易于阅读,使用和维护。 如果你没有明确地检查null ,我不知道是不是因为你认为没有人会传递一个null参数,而是指望它抛出exception,或者你忘记检查这个exception。

只要你犯了错误,你就会得到exception,而不是在你使用地图的时候,而不会理解为什么会发生。

它将一个看似不稳定的错误状态转化为明确的违反合同:该函数有正确工作的一些先决条件,所以事先检查它们,强制它们得到满足。

效果是,当你得到exception时,你不需要debuggingcomputeIfPresent() 。 一旦你看到exception来自前提检查,你就知道你用非法的参数调用了这个函数。 如果检查不存在,则需要排除computeIfPresent()内部computeIfPresent()导致抛出exception的一些错误的可能性。

显然,抛出通用的NullPointerException是一个非常糟糕的select,因为它本身并没有发出违反契约的信号。 IllegalArgumentException将是一个更好的select。


边注:
我不知道Java是否允许这样做(我怀疑它),但是C / C ++程序员在这种情况下使用了assert() ,这对于debugging来说是非常好的:它告诉程序立即崩溃并尽可能地提供的条件评估为false。 所以,如果你跑了

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

在一个debugging器下,并且有一些东西将NULL传递给任一参数,那么程序就会停止在告诉哪个参数为NULL的那一行,并且你可以在整个调用栈中检查所有的局部variables。

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

 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不会被实际使用的情况,即使c == null是可能的, NullPointerException也不会被抛出。

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

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