防御性编程:Java指南

8
我是来自.NET背景,现在正在涉猎Java。
目前,我在设计API时遇到了防御性地处理错误输入的大问题。假设我有以下代码(足够接近):
public void setTokens(Node node, int newTokens) {
    tokens.put(node, newTokens);
}

然而,此代码可能因以下两个原因而失败:
  1. 用户传递了一个 null 节点。
  2. 用户传递了一个无效节点,即不包含在图中的节点。

在 .NET 中,我会分别抛出一个 ArgumentNullException(而不是 NullReferenceException!)或一个 ArgumentException,并将有问题的参数名称 (node) 作为 string 参数传递。

Java 似乎没有等效的异常。我意识到我可以更具体地抛出最接近描述情况的任何异常,甚至为特定情况编写自己的异常类。

这是最佳实践吗?还是有类似于 .NET 中的 ArgumentException 的通用类?

在这种情况下检查null是否有意义?代码无论如何都会失败,异常的堆栈跟踪将包含上述方法调用。检查null似乎是多余和过度的。诚然,堆栈跟踪将更加干净(因为其目标是上面的方法,而不是JRE HashMap实现中的内部检查)。但这必须抵消额外if语句的成本,而且这种情况永远不应该发生 - 毕竟,将null传递给上面的方法不是预期的情况,而是相当愚蠢的错误。期望它是完全偏执的 - 即使我不进行检查,它也会以相同的异常失败。
[正如评论中指出的那样,HashMap.put实际上允许键的null值。因此,在这里检查null并不一定是多余的。]

2
如果你正在使用 Hashtable 或者 ConcurrentHashMap,那么调用 setTokens(null, 0) 只会抛出一个 NullPointerException,因为它们不允许使用 null 作为键。另一方面,HashMap 可以使用 null 作为键。 - pjp
@pjp:感谢您的更正-我本来就预料到会失败,因为HashMap需要创建其参数的哈希值。这也是显式检查的又一个原因,我想。 - Konrad Rudolph
10个回答

12

标准的Java异常是IllegalArgumentException。如果参数值为null,则有些人会抛出NullPointerException,但对我而言, NPE 意味着“某人弄糟了”,而且您不希望API的客户端认为您不知道自己在做什么。

对于公共API,请检查参数并尽早、干净地失败。时间/成本几乎不重要。


1
+1 对于IAE与NPE的争论。我认为(正确或错误)NPE是一种意外故障,而带有适当消息的IAE(经过显式空检查)则是明确的。 - Brian Agnew

7

在Java中,通常会抛出IllegalArgumentException异常。


2
顺便提一下,我们使用Jakarta Commons-Lang进行合同样式检查。 例如:Validate.notNull(node) 或 Validate.isTrue(node.isEmpty())这些将为您抛出IllegalArgumentExceptions。 - Fortyrunner
1
对于“通常”的某些值。我认为你会发现很多标准的Java API在传递空参数时会抛出NPE而不是IAE。 - Stephen C

7
不同的群体有不同的标准。首先,我假设您知道RuntimeException(未检查)和普通Exception(已检查)之间的区别,如果不知道,请参见this question and the answers。如果您编写自己的异常,可以强制其被捕获,而NullPointerExceptionIllegalArgumentException都是一些圈子中不受欢迎的RuntimeExceptions。其次,像您一样,我曾经与不使用asserts的团队合作,但如果您的团队(或API的使用者)决定使用asserts,则asserts听起来像是正确的机制。如果我是你,我会使用NullPointerException。原因在于先例。以Sun的一个Java API为例,例如java.util.TreeSet。这种情况正是使用NPE的原因,虽然它看起来像是您的代码只是用了一个null,但完全适当。
正如其他人所说,IllegalArgumentException是一种选择,但我认为NullPointerException更具有传达性。
如果这个API旨在供外部公司/团队使用,我会坚持使用NullPointerException,但要确保在javadoc中声明。如果是内部使用,则您可能决定添加自己的异常继承结构,但个人认为,那些只会被printStackTrace()或记录日志的大型异常继承结构的API只是浪费时间。
归根结底,最重要的是你的代码能够清晰地传达信息。局部异常继承结构就像局部行话-它为内部人员增加了信息,但可能会让外部人员感到困惑。
关于检查null,我认为这是有意义的。首先,它允许您在构造异常时添加关于什么是null(即节点或标记)的消息,这将很有帮助。其次,将来您可能会使用允许nullMap实现,然后您将失去错误检查。成本几乎为零,因此除非分析器表示这是一个内部循环问题,否则不必担心。

谢谢,这真的很有启发性。关于已检查和未检查异常:违反上述不变量是代码中的一个“错误”,而不是潜在的可行条件。在这里使用已检查的异常会非常疯狂 - 最好将NullPointerException作为已检查的异常,并在每个方法调用周围放置try块。 - Konrad Rudolph
当堆栈跟踪是解决问题的唯一线索时,为异常提供一个描述性的名称可能非常有帮助。 - Thorbjørn Ravn Andersen
@Thorbjørn 我通常更喜欢良好的消息而不是更好的异常命名,尽管两者都很有用。 - Nick Fortescue

2

看起来这可能是使用assert的适当用法:

public void setTokens(Node node, int newTokens) {
    assert node != null;
    tokens.put(node, newTokens);
}

那么,有人真的启用断言进行编译吗?我以为这已经过时了,基本上被单元测试和其他技术所取代。 - Konrad Rudolph
当然,我使用断言来检查内部状态。重要的是它们可以在现场启用。因此,如果程序表现出奇怪的行为,您可以使用断言运行它们,这可能会告诉您有关应用程序实际使用方式的一些有用信息,这可能没有被测试覆盖。但对于公共API,由于您无论如何都要明确测试它,因此断言是多余的。 - Ken

2

您的方法完全取决于函数向调用者提供的合同是什么 - node不为null是否是前提条件?

如果是,则如果node为null,则应抛出异常,因为这是合同违规。如果不是,则您的函数应该默默处理空节点并做出适当的响应。


2
如果您想了解如何编写良好的Java代码,我强烈推荐Joshua Bloch的书籍《Effective Java》作为指南。

那本书已经在我的愿望清单上了,但我目前的书籍预算已经超支了。 :-( - Konrad Rudolph
2
有史以来最好的Java书籍。可能是有史以来最好的编程书籍。 - Dónal
同意Don的观点。如果你经常使用Java,这本书应该是你愿望清单上任何其他东西的绝对首要选择。它涉及到你在问题中谈论的确切类型的事情。 - Jonik

1

个人而言,我希望NullPointerException只会在意外情况下发生,因此必须使用其他方法来指示传递了非法参数值。对于这一点,IllegalArgumentException是可以接受的。

if (arg1 == null) {
 throw new IllegalArgumentException("arg1 == null");
}

这应该足以让那些阅读代码的人和在凌晨3点接到支持电话的可怜人都能理解。

(并且,始终为您的异常提供解释性文本,某一天您会感激它们的存在)


1

我认为很多取决于方法的契约以及调用者的了解程度。

在流程的某个阶段,调用者可以采取行动来验证节点,然后再调用您的方法。如果您知道调用者并且知道这些节点始终得到验证,则可以假定您将获得良好的数据。基本上责任在调用者身上。

但是,如果您提供的是分发的第三方库,那么您需要验证节点是否为空等等...

IllegalArgumentException是Java标准,但也是RuntimeException。因此,如果您想强制调用者处理异常,则需要提供一个检查异常,可能是您创建的自定义异常。


0

就像其他异常一样:java.lang.IllegalArgumentException。 关于检查空节点,那么在节点创建时检查错误输入呢?


0

我不需要取悦任何人,所以我现在做的规范代码是

void method(String s) 

if((s != null) && (s instanceof String) && (s.length() > 0x0000))
{

这让我睡得更多了。

其他人可能会有不同的看法。


2
我不确定你需要进行instanceof检查。方法签名要求的是一个String或其子类,但是String是final的。 - Platinum Azure
@Platinum Azure:编译器问题,应该在编译时检查,但我曾经看到过在运行时可能会出现类转换异常的情况,所以我把它放在那里。似乎效果更好。当然也会有人持不同意见。顺便说一下,我对字符串进行了一些测试,发现它似乎会复制char[],因此char[0]++没有任何影响...任何有价值财产的大型商店都会编写自己的编译器,以避免像这样的未知问题。应该是标准的硕士级计算机科学水平。 - Nicholas Jordan

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