空参数应该抛出IllegalArgumentException还是NullPointerException?

574

我有一个简单的属性设置方法,对于这个特定的属性,null 是不合适的。在这种情况下,我一直很犹豫:是抛出 IllegalArgumentException,还是抛出 NullPointerException?从 javadocs 中来看,两者似乎都可以。是否有某种明确的标准?或者这只是其中一种情况,你应该按照自己的喜好去做,而且两种做法都是正确的呢?

26个回答

444
你应该使用IllegalArgumentException (IAE) 而不是 NullPointerException (NPE),原因如下:
首先,NPE JavaDoc 明确列出了使用 NPE 的情况。请注意,所有这些情况都是在不当地使用 null 时由运行时抛出的。相比之下,IAE JavaDoc 更加明确:“表示方法已收到一个不合法或不适当的参数。”没错,就是你!
其次,在堆栈跟踪中看到 NPE 时,你会做出什么假设?可能是有人对null进行了解引用。当你看到 IAE 时,你会假设堆栈顶部方法的调用者传递了非法值。再一次,后面的假设是正确的,前面的假设是误导性的。
第三,由于 IAE 明确设计用于验证参数,因此你必须将其视为异常的默认选择,那么为什么要选择 NPE 呢?显然不是为了实现不同的行为 - 你真的希望调用代码单独捕获 NPE 并因此采取不同的行动吗?你是想传达更具体的错误消息吗?但你可以在异常消息文本中实现,对于所有其他不正确的参数都应该这样做。
第四,所有其他不正确的参数数据都将是 IAE,那么为什么不能保持一致呢?为什么一个非法的null如此特别,以至于它值得作为所有其他类型非法参数的单独异常?
最后,我接受其他答案所给出的论点,即 Java API 的某些部分使用 NPE 这种方式。然而,Java API 在异常类型到命名约定的一切方面都是不一致的,因此我认为盲目地复制(你最喜欢的部分)Java API 不足以抵消这些其他考虑因素。

135
《Effective Java》第2版,第60条:"可以说,所有错误的方法调用都归结为非法参数或非法状态,但针对某些类型的非法参数和状态,通常使用其他异常。如果调用者在某个不允许使用null值的参数中传递了null,则惯例要求抛出NullPointerException而不是IllegalArgumentException。同样地,如果调用者在表示序列索引的参数中传递超出范围的值,则应抛出IndexOutOfBoundsException而不是IllegalArgumentException。" - Gili
35
JavaDoc中关于NPE的说明如下:"应用程序应该抛出此类的实例来指示对空对象的其他非法使用。" 这句话可以更加清晰明了 :( Translated: The JavaDoc中关于 NPE的说明如下:" 应用程序应该抛出此类的实例来指示对空对象的其他非法使用。" 这句话可以更加清晰明了 :( - R. Martinho Fernandes
13
不幸的是,验证方法Validate.notNull(commons lang)和Preconditions.checkNotNull(guava)都会抛出NPE :-( - Aaron Digulla
2
虽然Guava也有Preconditions.checkArgument()抛出IllegalArgumentException... - michaelok
17
从Guava文档中的@AaronDigulla:“我们意识到有许多有效的论据支持在空参数上抛出IAE异常。事实上,如果我们有一台时光机可以回到15年前,我们甚至可能会尝试推动事情朝这个方向发展。然而,我们已经决定坚持JDK和Effective Java偏爱NullPointerException的方式。如果您坚定地认为IAE是正确的,您仍然可以使用checkArgument(arg!= null),只是没有它返回参数的便利性,或者您可以为您的项目创建一个本地实用程序。” http://code.google.com/p/guava-libraries/wiki/IdeaGraveyard - Arto Bendiken
显示剩余8条评论

313

如果您不希望null成为可接受的值,那么可能会出现IllegalArgumentException。如果您尝试使用一个变量,但它是null,则会抛出NullPointerException异常。


35
下面这些回答提供了有力的论据,证明 NullPointerException 是正确的异常类型:https://dev59.com/HHVD5IYBdhLWcg3wXaYd#8160、https://dev59.com/HHVD5IYBdhLWcg3wXaYd#8196334 和 https://dev59.com/HHVD5IYBdhLWcg3wXaYd#6358。 - SamStephens
4
IllegalArgumentException与Java的Objects.requireNonNull(T)和Guava的Preconditions.checkNotNull(T)不一致,后者会抛出NullPointerException。但是,正如Jason Cohen的优秀回答及其评论部分中所解释的那样,“正确答案”肯定是使用IllegalArgumentException - matoni

171

标准做法是抛出 NullPointerException 异常。通常可靠的《Effective Java》在第一版的Item 42、第二版的Item 60或第三版的Item 72“优先使用标准异常”中简要讨论了这个问题:

“可以说,所有错误的方法调用都归结为非法参数或非法状态,但其他异常通常用于特定类型的非法参数和状态。如果调用方在某个禁止使用null值的参数中传递了 null 值,惯例规定应该抛出 NullPointerException 而不是 IllegalArgumentException。”


102
我强烈反对。NullPointerException只有在JVM意外跟随了一个空引用时才应该被抛出。这是区分你在凌晨三点被调用查看代码时的帮助所在。 - Thorbjørn Ravn Andersen
27
我并不一定同意标准(实际上在这个问题上我可以两种方式都行),但是JDK中一直使用的就是这个标准用法,因此《Effective Java》在为它做出辩护。我认为这是选择是否遵循标准或者做自己认为正确的事情的情况。除非你有非常好的理由(当然这也许就是一个),最好还是遵循标准惯例。 - GaryF
22
异常跟踪显示异常发生的位置,因此,如果异常类型的差异对您来说是一场噩梦或者是“帮助您的差异”,那么您正在做一些非常错误的事情。 - Jim Balter
9
幻想和诡辩。在此之下,MB写道:“注意,关于难以调试的论点是虚假的,因为你当然可以向NullPointerException提供一条消息,说明什么是null以及为什么它不应该是null。就像使用IllegalArgumentException一样。” - Jim Balter
13
作为原始回答者,让我说一下,这并不是很重要。这绝对不值得六年的讨论。选择一个标准,并保持一致即可。正如我指出的那样,标准是NPE。如果您因为某些原因更喜欢IAE,请使用它。只要保持一致即可。 - GaryF
显示剩余8条评论

152

我一直认为对于空参数应该抛出IllegalArgumentException,但是今天我注意到了Java 7中的java.util.Objects.requireNonNull方法。使用这个方法,代码不再是:

if (param == null) {
    throw new IllegalArgumentException("param cannot be null.");
}

你可以这样做:

Objects.requireNonNull(param);

如果您传递给方法的参数为null,它将抛出NullPointerException

考虑到该方法位于java.util的中心位置,我认为其存在非常明确地表明抛出NullPointerException是"Java的做法"。

无论如何,我想我已经决定了。

请注意,有关难以调试的论点是虚假的,因为您当然可以向NullPointerException提供消息,说明什么为空以及为什么不应为空。就像使用IllegalArgumentException一样。

NullPointerException的另一个优点是,在高度性能关键的代码中,您可以省略对空值的显式检查(和带有友好错误消息的NullPointerException),而只依赖于在null参数上调用方法时自动获取的NullPointerException。只要快速调用方法(即快速失败),则基本上具有相同的效果,只是对开发人员来说不太友好。 大多数情况下,最好明确检查并使用有用的消息抛出以指示哪个参数为空,但是如果性能要求而不会破坏方法/构造函数的公布合同,则有更改该选项的选择。


17
Guava的Preconditions.checkNotNull(arg)也会抛出NPE异常。 - assylias
16
这并不会给NullPointerException关于非法空参数添加更多的权重。JDK的requireNonNull()和Guava的checkNotNull()可以在代码中的任何位置调用,使用任何对象。例如,你可以在循环内部调用它们。requireNotNull()和checkNotNull()没有理由假设被调用时一定是带有某些方法参数,并抛出IllegalArgumentException异常。请注意,Guava还有一个Preconditions.checkArgument()方法,它会抛出IllegalArgumentException异常。 - Bogdan Calmac
2
Bogdan提出了一个很好的观点,尽管我怀疑requireNonNull的典型(并且通常是预期的)用途是用于参数检查。如果您需要在循环中检查某些内容是否为空,我认为断言会是典型的方式。 - MB.
21
我认为Object.requireNonNull()的JavaDoc强调了其作用:“该方法主要设计用于对方法和构造函数中的参数进行验证”。需要注意的是,翻译不会改变原文意思,只会使内容更加通俗易懂。 - Richard Zschech
请注意,支持隐式空检查的性能论点通常是无效的。JIT 能够识别用户空检查并省略随后的隐含空检查,并/或在任何类型的空检查上使用相同的优化集。有关更多信息,请参见此 wiki,特别是:_大多数情况下,用户编写的空检查在功能上与 JVM 插入的空检查完全相同。_当然,如果您在空值中执行某些高级操作,例如自定义消息,则此优化可能不适用。 - BeeOnRope

75

我倾向于遵循JDK库的设计,特别是Collections和Concurrency(Joshua Bloch、Doug Lea这些人知道如何设计可靠的API)。然而,JDK中许多API会主动抛出NullPointerException

例如,Map.containsKey的Javadoc说明如下:

@throws NullPointerException 如果键是null并且此映射不允许null键(可选)。

抛出自己的NPE是完全有效的。惯例是在异常消息中包括为null的参数名称。

模式如下:

public void someMethod(Object mustNotBeNull) {  
    if (mustNotBeNull == null) {  
        throw new NullPointerException("mustNotBeNull must not be null");  
    }  
}
无论做什么,都不要允许设置错误的值,并在其他代码尝试使用它时稍后抛出异常。这会使调试成为一场噩梦。您应始终遵循“快速失败”原则。

3
思考之食:也许 NullPointerException 没有继承 IllegalArgumentException 的原因是前者可能发生在不涉及方法参数的情况下。 - Gili
2
@Gili:也许这个问题本来就存在是因为Java不支持多重继承,如果Java支持MI的话,你就可以抛出一个既继承IllegalArgumentException又继承NullPointerException的异常了。 - Lie Ryan
7
这条信息不需要包含参数,因为它总是为null,这样会得到一条"null must not be null"的不太有用的信息。否则,我同意你可以创建一个带有有意义的信息的“丰富”NPE。 - PhiLho
5
我必须同意 - 当有疑问时,请遵循标准API。注意,并非API中的所有内容都是最佳的,但仍然由数百名开发人员维护和迭代,并被数百万程序员使用。现在,在Java 7中,我们有另一个使用NPE的示例; Objects.requireNonNull(T obj)方法 - 明确指定用于检查对象引用是否为非空,明确指定用于在方法/构造函数中进行参数验证,并明确指定如果对象为空,则抛出NPE。 - flamming_python

49

赞同Jason Cohen的观点,因为他表达得很清楚。让我逐步分解它。;-)

  • NPE JavaDoc明确表示“其他非法使用null对象”。如果仅限于运行时遇到不应该出现的null的情况,所有这些情况都可以更简洁地定义。

  • 如果您假设封装被正确应用,那么您就不必在意或注意错误地取消引用null和方法是否检测到不当null并引发异常。

  • 我选择NPE而不是IAE有多个原因

    • 它更具体地说明了非法操作的性质
    • 错误地允许null的逻辑通常与错误地允许非法值的逻辑非常不同。例如,如果我正在验证用户输入的数据,如果我获得一个不可接受的值,那么该错误的来源是应用程序的最终用户。如果我得到null,则是程序员的错误。
    • 无效值可能会导致堆栈溢出、内存不足错误、解析异常等。实际上,大多数错误通常在某个方法调用中呈现为无效值。因此,我认为IAE实际上是所有RuntimeException异常中最通用的异常。
  • 实际上,其他无效参数可能导致各种其他异常。 UnknownHostExceptionFileNotFoundException、各种语法错误异常、IndexOutOfBoundsException、身份验证失败等等。

  • 总的来说,我认为NPE很不幸,因为传统上它与未能遵循快速失败原则的代码相关联。加上JDK未能用消息字符串填充NPE,确实创建了一种强烈的负面情绪,这种情绪并不是建立在良好基础上的。事实上,从运行时角度看,NPE和IAE之间的区别仅仅是名称。从这个角度来看,你对名称越精确,就越能给调用者提供明确的信息。


    大多数未经检查的异常之间的区别仅在于名称。 - Thorbjørn Ravn Andersen

    22

    这是一个“圣战”风格的问题。换句话说,两个选择都不错,但人们会有自己的偏好,并且会为之而战。


    不,这个问题只有一个正确的答案,那就是在方法输入不正确时使用throw IllegalArgument异常。此外,在开发环境中,您可以使用断言来检查输入的有效性并抛出适当的异常 ;) - Mr.Q
    @Mr.Q 我认为应该抛出 NullPointerException:这是JDK使用的惯例,也要求在接口中使用,它更具体(就像 IndexOutOfBoundsException 等),等等。 - Solomon Ucko
    kekekekkek... :D - Yousha Aleayoub

    18

    如果这是一个setter方法并且传入了null,那么抛出IllegalArgumentException会更合理。在你试图使用null的情况下,NullPointerException似乎更合适。

    因此,如果您正在使用它且为null,则使用NullPointer。如果它被传入并且为null,则使用IllegalArgument


    10

    Apache Commons Lang拥有一个NullArgumentException,它可以做讨论中提到的一些事情:它扩展了IllegalArgumentException,其唯一的构造函数需要传入未应为null的参数名称。

    虽然我认为像NullArgumentException或IllegalArgumentException这样抛出异常更准确地描述了异常情况,但是我和同事们选择遵循Bloch在这个问题上的建议。


    8
    请注意,他们已经从commons-lang3中移除了它:http://apache-commons.680414.n4.nabble.com/commons-lang3-NullArgumentException-missing-td4222254.html - artbristol

    8
    实际上,在我看来,抛出IllegalArgumentException或NullPointerException只是Java异常处理中少数人的“圣战”,这些人对此理解不完整。一般来说,规则很简单,如下所示:
    • 必须尽快指示参数约束违规(->快速失败),以避免更难调试的非法状态
    • 无论什么原因导致的无效空指针,请抛出NullPointerException
    • 在数组/集合索引非法的情况下,请抛出ArrayIndexOutOfBounds
    • 在负面的数组/集合大小情况下,请抛出NegativeArraySizeException
    • 如果有一个未被以上覆盖且没有其他更具体的异常类型的非法参数,请将其作为废纸篓抛出IllegalArgumentException
    • 另一方面,在某些有效原因下无法通过快速失败避免的字段内的约束违规情况下,请捕获并重新抛出IllegalStateException或更具体的已检查异常。在这种情况下,永远不要让原始的NullPointerException、ArrayIndexOutOfBounds等通过!

    至少有三个很好的理由反对将所有种类的参数约束违规映射到IllegalArgumentException,第三个理由可能是如此严重,以至于标记该做法为不良风格:

    (1)程序员不能安全地假设所有的参数约束违规都会导致IllegalArgumentException,因为大多数标准类使用此异常作为废纸篓,如果没有更具体的异常类型可用。试图在API中将所有的参数约束违规映射到IllegalArgumentException只会导致使用您的类的程序员感到沮丧,因为标准库大多遵循不同的规则,违反了您的规则,而且大多数API用户也会使用它们!

    (2)映射异常实际上会导致一种不同种类的异常,这是由单继承引起的:所有Java异常都是类,因此只支持单继承。因此,无法创建既是NullPointerException又是IllegalArgumentException的异常,因为子类只能继承一个。因此,在空参数的情况下抛出IllegalArgumentException,这使得API用户更难以区分问题,例如通过将默认值馈入调用重复来编程地修正问题!

    (3)映射实际上会产生掩盖错误的危险:为了将参数约束违规映射到IllegalArgumentException,您需要在每个具有任何约束参数的方法中编写外部try-catch。然而,在此catch块中简单地捕获RuntimeException是不可能的,因为那会将由您的方法中使用的库方法抛出的已记录RuntimeException映射到IllegalArgumentException,即使它们不是由参数约束违规引起的。因此,您需要非常具体,但即使那样的努力也不能保护您免受意外将另一个API的未记录运行时异常(即错误)映射到您的API的IllegalArgumentException的情况。即使是最小心的映射也会冒着将其他库制造商的编程错误掩盖为您方法的用户的参数约束违规的风险,这只是可笑的行为!

    与标准做法相比,规则更简单,异常原因更明确。对于方法调用者而言,规则也很简单: - 如果遇到任何已记录的运行时异常,因为您传递了非法值,请使用默认值重复调用(对于这些特定的异常是必要的),或者纠正您的代码。 - 另一方面,如果您遇到一个未记录在案的运行时异常,针对给定的参数集合,向该方法的制造商提交错误报告,以确保他们的代码或文档得到修复。


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接