Java理论与实践: 关于异常的争论
副标题#e#
关于在 Java 语言中利用异常的大大都发起都认为,在确信异常可以被捕捉 的任何环境下,应该优先利用查抄型异常。语言设计(编译器强制您在要领签名 中列出大概被抛出的所有查抄型异常)以赶早期关于样式和用法的著作都支持该 发起。最近,几位著名的作者已经开始认为非查抄型异常在优秀的 Java 类设计 中有着比以前所认为的更为重要的职位。在本文中,Brian Goetz 考查了关于使 用非查抄型异常的优缺点。
与 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 技能利用履历。可是,厥后他们简直为查抄型异常的实现留出了空间 。)
#p#副标题#e#
对付查抄型异常的一些品评
#p#分页标题#e#
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);
}
#p#分页标题#e#
尽量该代码与四个 catch 块对比更为紧凑,可是它具有一个问题 —— 它还 捕捉大概由 doSomething 抛出的任何 RuntimeException 而且阻止它们举办扩 散。
过多的异常包装
假如异常是在一个底层的设施中生成的,而且通过很多代码层向上扩散,在 最终被处理惩罚之前它大概被捕捉、包装和从头抛出若干次。当异常最终被记录的时 候,栈跟踪大概有很多页,因为栈跟踪大概被复制多次,个中每个包装层一次。 (在 JDK 1.4 以及厥后的版本中,异常链的实此刻某种水平上缓解了该问题。 )
替换的要领
Bruce Eckel, Thinking in Java(请参阅 参考资料)的作者,声称在利用 Java 语言多年后,他已经得出这样的结论,认为查抄型异常是一个错误 —— 一个应该被声明为失败的试验。Eckel 倡导将所有的异常都作为非查抄型的,并 且提供清单 2 中的类作为将查抄型异常转变为非查抄型异常的一个要领,同时 保存当异常从栈向上扩散时捕捉特定范例的异常的本领(关于如何利用该要领的 表明,请参阅他在 参考资料小节中的文章):
清单 2. Eckel 的异常适配器类
class ExceptionAdapter extends RuntimeException {
private final String stackTrace;
public Exception originalException;
public ExceptionAdapter(Exception e) {
super(e.toString());
originalException = e;
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
stackTrace = sw.toString();
}
public void printStackTrace() {
printStackTrace(System.err);
}
public void printStackTrace(java.io.PrintStream s) {
synchronized(s) {
s.print(getClass().getName() + ": ");
s.print(stackTrace);
}
}
public void printStackTrace(java.io.PrintWriter s) {
synchronized(s) {
s.print(getClass().getName() + ": ");
s.print(stackTrace);
}
}
public void rethrow() { throw originalException; }
}
假如查察 Eckel 的 Web 站点上的接头,您将会发明回应者是严重破裂的。 一些人认为他的提议是谬妄的;一些人认为这是一个重要的思想。(我的概念是 ,尽量恰内地利用异常确实是很难的,而且对异常用欠好的例子大量存在,可是 大大都附和他的人是因为错误的原因才这样做的,这与一个政客位于一个可以随 便获取巧克力的平台上参选将会得到十岁孩子的大量选票的环境具有相似之处。 )
Rod Johnson 是 J2EE Design and Development(请参阅 参考资料) 的作 者,这是我所读过的关于 Java 开拓,J2EE 等方面的最好的书籍之一。他采纳 一个不太激进的要领。他罗列了异常的多个种别,而且为每个种别确定一个计策 。一些异常本质上是次要的返回代码(它凡是指示违反业务法则),而一些异常 则是“产生某种可骇错误”(譬喻数据库毗连失败)的变种。Johnson 倡导对付 第一种类此外异常(可选的返回代码)利用查抄型异常,而对付后者利用运行时 异常。在“产生某种可骇错误”的种别中,其念头是简朴地认识到没有挪用者能 够有效地处理惩罚该异常,因此它也大概以各类方法沿着栈向上扩散而对付中间代码 的影响保持最小(而且最小化异常沉没的大概性)。
Johnson 还罗列了一其中间景象,对此他提出一个问题,“只是少数挪用者 但愿处理惩罚问题吗?”对付这些景象,他也发起利用非查抄型异常。作为该类此外 一个例子,他罗列了 JDO 异常 —— 大大都环境下,JDO 异常暗示的环境是调 用者不但愿处理惩罚的,可是在某些环境下,捕捉和处理惩罚特定范例的异常是有用的。 他发起在这里利用非查抄型异常,而不是让其余的利用 JDO 的类通过捕捉和重 新抛出这些异常的形式来补充这个大概性。
利用非查抄型异常
#p#分页标题#e#
关于是否利用非查抄型异常的抉择是巨大的,而且很显然没有明明的谜底。 Sun 的发起是对付任何环境利用它们,而 C# 要领(也就是 Eckel 和其他人所 附和的)是对付任何环境都不利用它们。其他人说,“还存在一其中间景象。”
通过在 C++ 中利用异常,个中所有的异常都长短查抄型的,我已经发明非检 查型异常的最大风险之一就是它并没有凭据查抄型异常回收的方法那样自我文档 化。除非 API 的建设者明晰地文档化将要抛出的异常,不然挪用者没有步伐知 道在他们的代码中将要捕捉的异常是什么。不幸的是,我的履历是大大都 C++ API 的文档化很是差,而且纵然文档化很好的 API 也缺乏关于从一个给定要领 大概抛出的异常的足够信息。我看不出有任何来由可以说该问题对付 Java 类库 不是同样的常见,因为 Jav 类库严重依赖于非查抄型异常。依赖于您本身的或 者您的相助同伴的编程能力长短常坚苦的;假如不得不依赖于某小我私家的文档化技 巧,那么对付他的代码您大概得利用挪用栈中的十六个帧来作为您的主要的错误 处理惩罚机制,这将会是令人惊愕的。
文档化问题进一步强调为什么懒惰是导致选择利用非查抄型异常的一个欠好 的原因,因为对付文档化增加给包的承担,利用非查抄型异常应该比利用查抄型 异常甚至更高(当文档化您所抛出的非查抄型异常比查抄型异常变得更为重要的 时候)。
文档化,文档化,文档化
假如抉择利用非查抄型异常,您需要彻底地文档化这个选择,包罗在 Javadoc 中文档化一个要领大概抛出的所有非查抄型异常。Johnson 发起在每个 包的基本上选择查抄型和非查抄型异常。利用非查抄型异常时还要记着,纵然您 并不捕捉任何异常,也大概需要利用 try…finally 块,从而可以执行排除动 作譬喻封锁数据库毗连。对付查抄型异常,我们有 try…catch 用来提示增加 一个 finally 子句。对付非查抄型异常,我们则没有这个支撑可以依靠。