Java 理论与实践: 关于异常的争论
与 C++ 类似,Java 语言也提供异常的抛出和捕获。但是,与 C++ 不一样的是,Java 语言支持检查型和非检查型异常。Java 类必须在方法签名中声明它们所抛出的任何检查型异常,并且对于任何方法,如果它调用的方法抛出一个类型为 E 的检查型异常,那么它必须捕获 E 或者也声明为抛出 E(或者 E 的一个父类)。通过这种方式,该语言强制我们文档化控制可能退出一个方法的所有预期方式。
对于因为编程错误而导致的异常,或者是不能期望程序捕获的异常(解除引用一个空指针,数组越界,除零,等等),为了使开发人员免于处理这些异常,一些异常被命名为非检查型异常(即那些继承自 RuntimeException 的异常)并且不需要进行声明。
传统的观点
在下面的来自 Sun 的“The Java Tutorial”的摘录中,总结了关于将一个异常声明为检查型还是非检查型的传统观点(更多的信息请参阅 参考资料):
因为 Java 语言并不要求方法捕获或者指定运行时异常,因此编写只抛出运行时异常的代码或者使得他们的所有异常子类都继承自 RuntimeException ,对于程序员来说是有吸引力的。这些编程捷径都允许程序员编写 Java 代码而不会受到来自编译器的所有挑剔性错误的干扰,并且不用去指定或者捕获任何异常。尽管对于程序员来说这似乎比较方便,但是它回避了 Java 的捕获或者指定要求的意图,并且对于那些使用您提供的类的程序员可能会导致问题。
检查型异常代表关于一个合法指定的请求的操作的有用信息,调用者可能已经对该操作没有控制,并且调用者需要得到有关的通知 ?? 例如,文件系统已满,或者远端已经关闭连接,或者访问权限不允许该动作。
如果您仅仅是因为不想指定异常而抛出一个 RuntimeException,或者创建 RuntimeException 的一个子类,那么您换取到了什么呢?您只是获得了抛出一个异常而不用您指定这样做的能力。换句话说,这是一种用于避免文档化方法所能抛出的异常的方式。在什么时候这是有益的?也就是说,在什么时候避免注明一个方法的行为是有益的?答案是“几乎从不。”
换句话说,Sun 告诉我们检查型异常应该是准则。该教程通过多种方式继续说明,通常应该抛出异常,而不是 RuntimeException ?? 除非您是 JVM。
在 Effective Java: Programming Language Guide 一书中(请参阅 参考资料),Josh Bloch 提供了下列关于检查型和非检查型异常的知识点,这些与 “The Java Tutorial” 中的建议相一致(但是并不完全严格一致):
第 39 条:只为异常条件使用异常。也就是说,不要为控制流使用异常,比如,在调用 Iterator.next() 时而不是在第一次检查 Iterator.hasNext() 时捕获 NoSuchElementException。
第 40 条:为可恢复的条件使用检查型异常,为编程错误使用运行时异常。这里,Bloch 回应传统的 Sun 观点 ?? 运行时异常应该只是用于指示编程错误,例如违反前置条件。
第 41 条:避免不必要的使用检查型异常。换句话说,对于调用者不可能从其中恢复的情形,或者惟一可以预见的响应将是程序退出,则不要使用检查型异常。
第 43 条:抛出与抽象相适应的异常。换句话说,一个方法所抛出的异常应该在一个抽象层次上定义,该抽象层次与该方法做什么相一致,而不一定与方法的底层实现细节相一致。例如,一个从文件、数据库或者 JNDI 装载资源的方法在不能找到资源时,应该抛出某种 ResourceNotFound 异常(通常使用异常链来保存隐含的原因),而不是更底层的 IOException、SQLException 或者 NamingException。
重新考察非检查型异常的正统观点
最近,几位受尊敬的专家,包括 Bruce Eckel 和 Rod Johnson,已经公开声明尽管他们最初完全同意检查型异常的正统观点,但是他们已经认定排他性使用检查型异常的想法并没有最初看起来那样好,并且对于许多大型项目,检查型异常已经成为一个重要的问题来源。Eckel 提出了一个更为极端的观点,建议所有的异常应该是非检查型的;Johnson 的观点要保守一些,但是仍然暗示传统的优先选择检查型异常是过分的。(值得一提的是,C# 的设计师在语言设计中选择忽略检查型异常,使得所有异常都是非检查型的,因而几乎可以肯定他们具有丰富的 Java 技术使用经验。但是,后来他们的确为检查型异常的实现留出了空间。)
对于检查型异常的一些批评
Eckel 和 Johnson 都指出了一个关于检查型异常的相似的问题清单;一些是检查型异常的内在属性,一些是检查型异常在 Java 语言中的特定实现的属性,还有一些只是简单的观察,主要是关于检查型异常的广泛的错误使用是如何变为一个严重的问题,从而导致该机制可能需要被重新考虑。
检查型异常不适当地暴露实现细节
您已经有多少次看见(或者编写)一个抛出 SQLException 或者 IOException 的方法,即使它看起来与数据库或者文件毫无关系呢?对于开发人员来说,在一个方法的最初实现中总结出可能抛出的所有异常并且将它们增加到方法的 throws 子句(许多 IDE 甚至帮助您执行该任务)是十分常见的。这种直接方法的一个问题是它违反了 Bloch 的 第 43 条 ?? 被抛出的异常所位于的抽象层次与抛出它们的方法不一致。
一个用于装载用户概要的方法,在找不到用户时应该抛出 NoSuchUserException,而不是 SQLException ?? 调用者可以很好地预料到用户可能找不到,但是不知道如何处理 SQLException。异常链可以用于抛出一个更为合适的异常而不用丢弃关于底层失败的细节(例如栈跟踪),允许抽象层将位于它们之上的分层同位于它们之下的分层的细节隔离开来,同时保留对于调试可能有用的信息。
据说,诸如 JDBC 包的设计采取这样一种方式,使得它难以避免该问题。在 JDBC 接口中的每个方法都抛出 SQLException,但是在访问一个数据库的过程中可能会经历多种不同类型的问题,并且不同的方法可能易受不同错误模式的影响。一个 SQLException 可能指示一个系统级问题(不能连接到数据库)、逻辑问题(在结果集中没有更多的行)或者特定数据的问题(您刚才试图插入行的主键已经存在或者违反实体完整性约束)。如果没有犯不可原谅的尝试分析消息正文的过失,调用者是不可能区分这些不同类型的 SQLException 的。(SQLException 的确支持用于获取数据库特定错误代码和 SQL 状态变量的方法,但是在实践中这些很少用于区分不同的数据库错误条件。)
不稳定的方法签名
不稳定的方法签名问题是与前面的问题相关的 ?? 如果您只是通过一个方法传递异常,那么您不得不在每次改变方法的实现时改变它的方法签名,以及改变调用该方法的所有代码。一旦类已经被部署到产品中,管理这些脆弱的方法签名就变成一个昂贵的任务。然而,该问题本质上是没有遵循 Bloch 提出的第 43 条的另一个症状。方法在遇到失败时应该抛出一个异常,但是该异常应该反映该方法做什么,而不是它如何做。
有时,当程序员对因为实现的改变而导致从方法签名中增加或者删除异常感到厌烦时,他们不是通过使用一个抽象来定义特定层次可能抛出的异常类型,而只是将他们的所有方法都声明为抛出 Exception。换句话说,他们已经认定异常只是导致烦恼,并且基本上将它们关闭掉了。毋庸多言,该方法对于绝大多数可任意使用的代码来说通常不是一个好的错误处理策略。
难以理解的代码
因为许多方法都抛出一定数目的不同异常,错误处理的代码相对于实际的功能代码的比率可能会偏高,使得难以找到一个方法中实际完成功能的代码。异常是通过集中错误处理来设想减小代码的,但是一个具有三行代码和六个 catch 块(其中每个块只是记录异常或者包装并重新抛出异常)的方法看起来比较膨胀并且会使得本来简单的代码变得模糊。
异常淹没
我们都看到过这样的代码,其中捕获了一个异常,但是在 catch 块中没有代码。尽管这种编程实践很明显是不好的,但是很容易看出它是如何发生的 ?? 在原型化期间,某人通过 try...catch 块包装代码,而后来忘记返回并填充 catch 块。尽管这个错误很常见,但是这也是更好的工具可以帮助我们的地方之一 ?? 对于异常淹没的地方,通过编辑器、编译器或者静态检查工具可以容易地检测并发出警告。
极度通用的 try...catch 块是另一种形式的异常淹没,并且更加难以检测,因为这是 Java 类库中的异常类层次的结构而导致的(可疑)。让我们假定一个方法抛出四个不同类型的异常,并且调用者遇到其中任何一个异常都将捕获、记录它们,并且返回。实现该策略的一种方式是使用一个带有四个 catch 子句的 try...catch 块,其中每个异常类型一个。为了避免代码难以理解的问题,一些开发人员将重构该代码,如清单 1 所示:
清单 1. 意外地淹没 RuntimeException
try {
doSomething();
}
catch (Exception e) {
log(e);
}
尽管该代码与四个
补充:软件开发 , Java ,