【问题标题】:Defensive Programming: Guidelines in Java防御性编程:Java 指南
【发布时间】:2009-09-18 12:44:02
【问题描述】:

我来自 .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 传递给上述方法不是预期的情况,这是一个相当愚蠢的错误。期待它是彻头彻尾的偏执 - 即使我不检查它也会失败并出现同样的异常。

[正如 cmets 中所指出的,HashMap.put 实际上允许 null 键值。所以在这里检查null 不一定是多余的。]

【问题讨论】:

  • 调用 setTokens(null,0) 只会在您使用 HashtableConcurrentHashMap 时抛出 NullPointerException,因为它们不允许使用 null 键。另一方面,HashMap 很高兴拥有空键。
  • @pjp:感谢您的更正——我原以为会失败,因为HashMap 需要创建其参数的哈希值。我猜还有一个明确检查的原因。

标签: java exception defensive-programming


【解决方案1】:

标准 Java 异常是 IllegalArgumentException。如果参数为空,有些人会抛出NullPointerException,但对我来说,NPE 有“有人搞砸了”的含义,你不希望你的 API 的客户认为你不知道自己在做什么。

对于公共 API,请检查参数并尽早彻底地失败。时间/成本几乎无关紧要。

【讨论】:

  • +1 用于 IAE 与 NPE 参数。我假设(正确或错误)NPE 是不可预见的错误,而 IAE(带有适当的消息)遵循明确的空检查
【解决方案2】:

不同的群体有不同的标准。

首先,我假设您知道RuntimeExceptions(未选中)和普通Exceptions(选中)之间的区别,如果不知道,请参阅this question and the answers。如果您编写自己的异常,则可以强制捕获它,而 NullPointerExceptionIllegalArgumentException 都是 RuntimeExceptions,在某些圈子中不受欢迎。

其次,与您一样,我曾与之合作但不积极使用断言的小组,但如果您的团队(或 API 的使用者)决定使用断言,那么断言听起来正是正确的机制。

如果我是你,我会使用NullPointerException。其原因是先例。以 Sun 的 Java API 为例,例如java.util.TreeSet。这正是在这种情况下使用 NPE,虽然看起来您的代码确实使用了 null,但它是完全合适的。

正如其他人所说,IllegalArgumentException 是一种选择,但我认为 NullPointerException 更具交流性。

如果此 API 旨在供外部公司/团队使用,我会坚持使用 NullPointerException,但请确保在 javadoc 中声明它。如果它是供内部使用的,那么您可能会认为添加自己的异常层次结构是值得的,但我个人发现添加大量异常层次结构的 API 只会是 printStackTrace()d 或记录的只是浪费精力。

归根结底,最重要的是您的代码可以清晰地交流。本地异常层次结构就像本地行话 - 它为内部人员添加信息,但会使外部人员感到困惑。

关于检查 null 我认为它确实是有道理的。首先,它允许您在构造异常时添加关于什么为空(即节点或令牌)的消息,这将很有帮助。其次,将来您可能会使用允许nullMap 实现,然后您将丢失错误检查。成本几乎为零,所以除非分析器说这是一个内部循环问题,否则我不会担心。

【讨论】:

  • 谢谢,这真的很丰富。关于已检查与未检查的异常:违反上述不变量是代码中的 bug,而不是潜在的可行条件。在这里使用受检异常会很疯狂——不妨将NullPointerException 设为受检异常,并在每个方法调用周围放置一个try 块。
  • 当堆栈跟踪是解决问题的唯一线索时,异常的描述性 name 可能非常有用。
  • @Thorbjørn 我通常更喜欢好的消息而不是更好命名的异常,尽管显然两者都很有用
【解决方案3】:

在 Java 中,您通常会抛出 IllegalArgumentException

【讨论】:

  • 顺便说一句。我们使用 Jakarta Commons-Lang 进行合同样式检查。例如。 Validate.notNull(node) 或 Validate.isTrue(node.isEmpty()) 这些会为你抛出 IllegalArgumentExceptions
  • 对于“正常”的一些值。我认为您会发现很多标准 Java API 在传递空参数时会优先抛出 NPE 而不是 IAE。
【解决方案4】:

如果您想了解如何编写好的 Java 代码,我强烈推荐 Joshua Bloch 的《Effective Java》一书。

【讨论】:

  • 那本书已经在我的愿望清单上,但我目前的书籍预算远远超出了我的预算。 :-(
  • 有史以来最好的 Java 书籍。可能是有史以来最好的编程书籍。
  • 同意唐。如果您经常使用 Java,那么这本书应该绝对高于您的愿望清单上的任何其他内容。它处理的正是您在问题中谈到的那种事情。
【解决方案5】:

听起来这可能是assert 的合适用法:

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

【讨论】:

  • 真的有人在编译时启用了断言吗?我认为它们已经过时了,几乎被单元测试和其他技术所取代。
  • 当然,我使用断言来检查内部状态。重要的是它们可以在现场启用。因此,如果程序行为异常,您可以让它们在断言运行的情况下运行,它可能会告诉您有关应用程序实际使用方式的有用信息,而这些信息可能未被测试覆盖。但是对于公共 API,由于无论如何您都将对其进行显式测试,因此断言是多余的。
【解决方案6】:

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

如果是,那么如果 node 为空,则应该抛出异常,因为它违反了合同。如果不是,那么您的函数应该静默处理空节点并做出适当的响应。

【讨论】:

    【解决方案7】:

    我认为很大程度上取决于方法的合同以及调用者的了解程度。

    在过程中的某个时刻,调用者可以在调用您的方法之前采取行动来验证节点。如果您认识调用者并且知道这些节点总是经过验证,那么我认为可以假设您将获得良好的数据。本质上,责任在调用者身上。

    但是,例如,如果您要提供分布式的第三方库,那么您需要验证节点是否存在空值等...

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

    【讨论】:

      【解决方案8】:

      我个人希望 NullPointerExceptions 只是偶然发生,因此必须使用其他东西来指示传递了非法参数值。 IllegalArgumentException 对此很好。

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

      这对于阅读代码的人来说应该足够了,对于凌晨 3 点接到支持电话的可怜人来说也足够了。

      (并且,总是为您的例外情况提供解释性文字,您会在悲伤的一天感谢他们)

      【讨论】:

        【解决方案9】:

        像另一个:java.lang.IllegalArgumentException。 关于检查空节点,在创建节点时检查错误输入呢?

        【讨论】:

          【解决方案10】:

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

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

          这让我睡了很多觉。

          其他人会不同意。

          【讨论】:

          • 我不确定您是否需要进行 instanceof 检查。方法签名需要一个字符串或一个字符串的子类,但字符串是最终的。
          • @Platinum Azure:编译器问题,应该在编译时检查,但我已经看到它可以在运行时获得类转换异常,所以我只是把它放在那里。似乎工作得更好。会有不同意的人。顺便说一句,我对字符串进行了一些测试,它似乎制作了 char[] 的副本,所以 char[0]++ 没有任何效果......任何在线拥有有价值财产的大商店都会编写自己的编译器以避免未知在这样的问题上。应该是标准的masers级别的cs。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2011-01-15
          • 2015-12-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多