为什么显式抛出一个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
意味着所讨论的方法没有副作用(因为它失败了方法前置条件)。
顺便提一句,如果你提出了像ArgumentNullException
或IllegalArgumentException
这样的事情,这是做检查点的一部分:你想要一个不同的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()
更好。
有意保护进一步的损害,或进入不一致的状态。