何时选择已检查和未检查异常

267
在Java(或其他具有已检异常的语言)中,当创建自己的异常类时,如何决定它应该是已检异常还是未检异常?
我的直觉是,在调用者可能可以以某种有益的方式恢复的情况下,应该使用已检异常,而未检异常则更适用于不可恢复的情况,但我很想听听其他人的想法。

15
Barry Ruzek撰写了一份优秀指南,介绍如何选择使用已检查异常或未检查异常。 - sigget
18个回答

3
我们需要根据异常是程序员错误还是非程序员错误来区分这两种异常类型。
- 如果一个错误是程序员错误,那么它必须是未检查异常。例如:SQLException/IOException/NullPointerException。这些异常是编程错误,应该由程序员处理。虽然在JDBC API中,SQLException是已检查异常,在Spring JDBCTemplate中它是未检查异常。当使用Spring时,程序员不必担心SqlException。 - 如果一个错误不是程序员错误,而是来自外部的原因,那么它必须是已检查异常。例如:如果文件被删除或文件权限被其他人更改,则应该进行恢复。
FileNotFoundException是理解微妙差别的好例子。如果文件路径由开发人员定义或通过GUI从最终用户获取,则应该是未检查异常。如果文件被其他人删除,则应该是已检查异常。
已检查异常可以通过try-catch或传播异常的方式来处理。在传播异常的情况下,由于异常处理,调用堆栈中的所有方法将紧密耦合。因此,我们必须谨慎使用已检查异常。
如果您开发分层企业系统,则大多数情况下选择抛出未检查异常,但不要忘记对于无法处理的情况使用已检查异常。

3

以下是我多年开发经验后的观点:

  1. 已检查异常。这是业务用例或调用流程的一部分,也是我们预期或不预期的应用程序逻辑的一部分。例如连接被拒绝、条件不满足等。我们需要处理它,并向用户显示相应的消息,说明发生了什么以及下一步该怎么做(稍后再试等)。 我通常称之为后处理异常或“用户”异常。

  2. 未检查异常。这是编程异常的一部分,是软件代码编程中的一些错误(bug、缺陷),并反映了程序员必须按照文档使用API的方式。如果外部库/框架文档说它希望在某个范围内获得数据并且非空,因为将抛出NPE或IllegalArgumentException,程序员应该按照文档正确使用API。否则,将抛出异常。 我通常称之为预处理异常或“验证”异常。

根据目标受众。现在让我们谈谈异常的目标受众或设计人群(根据我的观点):

  1. 已检查异常。目标受众是用户/客户。
  2. 未检查异常。目标受众是开发人员。换句话说,未检查异常仅为开发人员设计。

按应用程序开发生命周期阶段划分。

  1. 已检查异常旨在在整个生产生命周期中存在,作为应用程序处理异常情况的正常和预期机制。
  2. 未检查异常仅在应用程序开发/测试生命周期中存在,所有这些异常都应在此期间内修复,并且不应在应用程序已在生产环境中运行时抛出。

框架通常使用未检查异常(例如Spring)的原因是框架无法确定您的应用程序的业务逻辑,这取决于开发人员捕获并设计自己的逻辑。


2

我同意在设计API时,作为规则使用未检查的异常。调用者总是可以选择捕获已记录的未检查异常。你只是不需要无端地强制调用者这样做。

我发现在低级别上,使用已检查异常作为实现细节非常有用。它通常似乎比管理指定的错误“返回代码”更好的控制流机制。它有时还可以帮助看到低级别代码更改的影响...声明下游的已检查异常并查看谁需要进行调整。如果有很多通用的catch(Exception e)throws Exception,则最后一点并不适用,这通常也不是太经过深思熟虑。


1

我认为我们可以考虑从几个问题中豁免:

为什么会发生异常?当它发生时我们该怎么办

由于错误,出现了一个漏洞。例如调用了空对象的方法。

String name = null;
... // some logics
System.out.print(name.length()); // name is still null here

这种异常应该在测试期间修复。否则,它会破坏生产环境,并且您将得到一个非常高的错误,需要立即修复。这种异常不需要进行检查。
从外部输入时,您无法控制或信任外部服务的输出。
String name = ExternalService.getName(); // return null
System.out.print(name.length());    // name is null here

在这里,如果名称为空,则需要检查是否为null,如果想要继续,否则可以让它不变,并在此处停止并给调用者运行时异常。这种异常无需检查。

通过外部的运行时异常,您无法控制或信任外部服务。

在这里,如果发生异常,您可能需要捕获来自ExternalService的所有异常,如果想要继续,否则可以让它不变,并在此处停止并给调用者运行时异常。

通过外部的已检查异常,您无法控制或信任外部服务。

在这里,如果发生异常,您可能需要捕获来自ExternalService的所有异常,如果想要继续,否则可以让它不变,并在此处停止并给调用者运行时异常。

在这种情况下,我们需要知道ExternalService中发生了什么样的异常吗?这取决于:

  1. 如果您可以处理某些类型的异常,则需要捕获并处理它们。对于其他异常,请将其冒泡。

  2. 如果您需要记录或向用户响应特定的异常,则可以捕获它们。对于其他异常,请将其冒泡。


1
每当一个异常不太可能发生,我们可以在捕获后继续执行,并且我们无法避免该异常时,我们可以使用已检查的异常。
每当我们想要在特定异常发生时做一些有意义的事情,并且该异常是预期但不确定的时,我们可以使用已检查的异常。
每当异常在不同层中导航时,我们不需要在每个层中都捕获它,在这种情况下,我们可以使用运行时异常或将异常包装为未检查的异常。
当异常最有可能发生时,使用运行时异常,没有其他方法可以继续并且无法恢复。因此,在这种情况下,我们可以针对该异常采取预防措施。例如:NUllPointerException,ArrayOutofBoundsException。这些更有可能发生。在这种情况下,我们可以在编码时采取预防措施以避免此类异常。否则,我们将不得不在每个地方编写try catch块。
更一般的异常可以变成未检查的异常,而较少一般的则是已检查的异常。

1

已检查异常适用于可恢复的情况,其中您希望向调用者提供信息(即权限不足、文件未找到等)。

未经检查的异常很少使用,如果有的话,仅用于在运行时通知用户或程序员发生严重错误或意外情况。如果您正在编写将由他人使用的代码或库,请勿抛出它们,因为他们可能不会期望您的软件抛出未经检查的异常,因为编译器不强制捕获或声明它们。


1
我不同意你的说法:“未检查的异常很少使用,如果有的话”,事实上应该相反!在设计应用程序异常层次结构时,默认情况下使用未检查的异常。让开发人员决定何时要处理异常(例如,如果他们不知道如何处理异常,则不强制放置catch块或放置throws子句)。 - user1697575

0
我认为在声明应用程序异常时,它应该是未检查的异常,即RuntimeException的子类。原因是它不会在方法上使应用程序代码混乱,需要使用try-catch和throws声明。如果您的应用程序正在使用抛出已检查异常的Java Api,则无论如何都需要处理。对于其他情况,应用程序可以抛出未经检查的异常。如果应用程序调用者仍然需要处理未经检查的异常,则可以这样做。

-13
我使用的规则是:永远不要使用未经检查的异常!(除非你找不到其他解决方法)
从使用你的库的开发者或使用你的库/应用程序的最终用户的角度来看,由于未捕获的异常而导致应用程序崩溃真的很糟糕。而且依赖于一个 catch-all 也不好。
这样,最终用户仍然可以看到错误消息,而不是应用程序完全消失。

2
你没有解释为什么对于大多数未检查异常使用一个捕获所有的方法是错误的。 - Matthew Flaschen
1
完全不同意你没有论据的回答:“永远不要使用未经检查的异常”。 - user1697575

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