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

时间:2017-05-12 02:55:25

标签: 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;
}

9 个答案:

答案 0 :(得分:249)

有许多理由浮现在脑海中,其中一些原因密切相关:

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

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

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

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

答案 1 :(得分:39)

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

如果在方法的顶部没有保护条款,请考虑会发生什么。即使在NPE被抛出之前hash(key)已传入getNode(hash, key),也会始终致电nullremappingFunction

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

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

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

答案 2 :(得分:25)

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

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

这取决于Java解释器和JIT编译器检测空指针的解除引用所采用的策略。一种策略是不测试null,而是捕获当指令试图访问地址0时发生的SIGSEGV。这是参考始终有效但是昂贵在NPE案件中。

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

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

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

答案 3 :(得分:20)

除了其他人指出的内容之外,值得注意的是会议的作用。例如,在C#中,你也有同样的惯例,在这种情况下明确地引发异常,但它特别是ArgumentNullException,这更具体一些。 (C#约定是NullReferenceException 总是表示某种类型的错误 - 很简单,它不应该永远在生产代码中发生;被授予, ArgumentNullException通常也会这样做,但它可能更像是一个错误,你不会理解如何正确地使用这个库&#34;那种错误。)

所以,基本上,在C#中NullReferenceException意味着你的程序实际上试图使用它,而ArgumentNullException意味着它认识到这个值是错误的并且它甚至没有打扰到试着用它。影响实际上可能是不同的(取决于具体情况)因为ArgumentNullException意味着所讨论的方法还没有副作用(因为它没有通过方法前置条件)。

顺便提一下,如果您提出ArgumentNullExceptionIllegalArgumentException之类的内容,那就是进行检查的部分内容:您需要一个与您不同的例外情况&#39; d&#34;通常&#34;得到。

无论哪种方式,明确提出异常都强化了明确说明方法的前提条件和预期参数的良好做法,这使得代码更易于阅读,使用和维护。如果您没有明确检查null,我不知道是否因为您认为没有人会通过null论证,您可以无论如何,重新计算它以抛出异常,或者你只是忘了检查它。

答案 4 :(得分:12)

这样你就会在你犯错误时立即得到异常,而不是在你使用地图时会得到异常,并且不会理解它为什么会发生。

答案 5 :(得分:9)

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

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

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

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

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

    ...
}

在一个调试器下,有些东西将NULL传递给任一个参数,程序会在该行停止,告诉哪个参数是NULL,你可以检查整个局部变量的所有局部变量闲暇时拨打电话。

答案 6 :(得分: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()之类的一般性检查更好的原因。

答案 7 :(得分:5)

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

答案 8 :(得分:0)

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

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

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

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

简短而无益...

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

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

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

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

repository.getService(someObject.someMethod());

如果获得NPE并指向该行,则repositorysomeObject中哪一个为空?

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

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

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