【问题标题】:Should exceptions be thrown symmetrically?是否应该对称抛出异常?
【发布时间】:2013-12-23 08:34:23
【问题描述】:

考虑一个哈希表,如果 getput 操作的 key 为 null,它将引发 NullPointerException (NPE)。这让我想象异常应该是对称的,即 NPE 对于所有涉及空节点输入参数的哈希表操作应该是一致的。

我的理解正确吗?

如果是,那么考虑一个示例类 Graph 的情况,它在下面使用了一个 HashMap。它旨在为空输入抛出 NPE。无需了解其内部结构,它有两个功能:

graph.addNode (node);  // throws NPE if node is null.
graph.getEdges (node); // Should empty collections be returned 
                       // or NPE for null node ? 

根据Effective Java,我们应该更喜欢返回空集合而不是null。现在,如果节点参数为空并且我们想要获取它的边,map.get(null node) 将返回一个空集合。是否仍然首选返回 NPE 以保持对称性?

【问题讨论】:

  • 如果 node 不期望为 null 则抛出 NPE,因为对于这些您不期望的情况会出现异常。否则返回一个空集合。我同意@duncan 的观点,即他是基于观点的,因为这里有不止一种方法可以使用
  • NPE 可能更一致,但空集合可能更易于使用...
  • 这对我来说是一个非常有效的问题。
  • @JBNizet 经过进一步思考并看到您的出色回答,我撤回了我的投票。我想这里的答案很明确。

标签: java exception


【解决方案1】:

这不是对称的问题。这是一个合同问题,以及对这份合同的尊重。一个方法应该定义一个关于它接受什么作为参数、它做什么以及它返回和抛出什么的协定。如果调用者不遵守合同,那么这是一个开发错误,应该由运行时异常发出信号。

在调用方不遵守合约时返回有效结果是一个坏主意:它隐藏了错误而不是发出信号,这通常会使问题变得更糟,更难找到。

在您的示例中,将空节点添加到图表是没有意义的。所以应该在adddNode()javadoc中写到该方法不接受null,该方法应该抛出NullPointerException。

同样,请求空节点的边也没有任何意义。这样做的开发人员可能忘记了初始化变量或类似的东西。没有正当理由询问 null 的边缘。所以应该记录getEdges()不接受null,如果null作为参数传递,该方法应该抛出NullPointerException。

当我教这个时,我经常使用下面的例子。假设您创建了一个名为boolean hasCancer(ExaminationResults results) 的方法。这种方法必须分析结果并判断患者是否患有癌症。如果传递的结果为空,它应该返回 true 吗?即使你不知道他是否真的患有癌症,这也会使患者接受癌症治疗。那么它应该返回false吗?好吧,患者可能患有癌症,并且可能会死亡,因为该方法告诉他没有癌症,而实际上并不知道。所以该方法不应该返回任何东西。它应该通过抛出 NullPointerException 来发出错误信号。

getEdges() 返回一个空集合是否与我的癌症示例相当?你不知道。也许您的图表的最终用户使用它来诊断癌症,并且也许使用空边集合来确定患者患有癌症。所以快速失败,并抛出 NullPointerException。

【讨论】:

  • 您的示例并不真正适用于 IMO - 空集合与任意布尔值不同,它是一个合理的返回值,即使是 null。
  • 很好的答案,但我认为您的癌症示例可以改进。更接近的示例是返回建议治疗的列表(例如List<Medicine> hasCancer(ExaminationResults results))。那么问题是 - 你是否在无效输入上返回一个空列表。
  • @Duncan:这样的例子确实更接近于 OP 的图形例子。但道理是一样的。
  • @assylias 我想你可以做任何你想做的事,只要它有据可查。但我同意 JB 的观点,即在 null 输入没有意义的情况下,NPE 会更合适。
  • @assylias:以 Duncan 为例:当传递的结果参数为空时,返回一个空的药物列表是个好主意吗?不,不会。
【解决方案2】:

当客户端调用一个方法时,该方法应该fail-fast,因为比检测错误更容易。它们出现在它们第一次出现的地方。

在您的情况下,您有一个名为 addNode(node) 的方法。因此,当客户端调用它来添加节点时,行为取决于该方法的合同。我猜你不希望客户端调用它并想要添加一个null 节点。因此,您应该 fail-fast 并抛出 NullPointerException 或者 IllegalArgumentException

在第二种情况下node.getEdges(node) 它也取决于合同,合同取决于在您的问题领域中什么是有意义的。我认为node.getEdges(null) 应该抛出一个IllegalArgumentException,因为没有边的节点与null 节点不同,我想对此进行区分。

关于返回值的更多考虑

如果客户端从某个方法获得结果,该结果应该使客户端代码更容易编写和容错

在第二种情况getEdges(node) 中,如果节点没有边,您应该返回一个空集合,因为这使客户端代码更容易。客户端可以避免不必要的null 检查。返回值时,您应该考虑客户端代码以及它将如何处理返回值。那么你应该选择一个让客户端代码更容易的返回值。

例如:我希望getEdges(node) 的客户端代码看起来像这样

 Collection<Node> nodes = grap.getEdges(node);
 for(Node node: nodes){
     ...
 }

因此,如果您返回 null 而不是空集合,则客户端代码必须如下所示

 Collection<Node> nodes = grap.getEdges(node);
 if(nodes != null){
     for(Node node: nodes){
         ...
     }
 }

客户端的最坏情况是如果getEdges(node) 方法在代码没有边时抛出NullPointerException。我认为在图中某些节点不存在边是正常情况。因此我不会抛出异常。

这就是为什么在这种情况下我会返回一个空集合。

【讨论】:

  • 我同意,但问题不是当节点没有边时返回什么。问题是如果将null 传递给getEdges() 会返回什么。 null 与“没有边的节点”非常不同。
猜你喜欢
  • 2012-06-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-07-21
  • 1970-01-01
  • 2013-05-20
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多