|
![]() |
名片设计 CorelDRAW Illustrator AuotoCAD Painter 其他软件 Photoshop Fireworks Flash |
|
与 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); } 尽管该代码与四个 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 的类通过捕捉和重新抛出这些非常的形式来弥补这个可能性。 使用非检查型非常 关于是否使用非检查型非常的决定是复杂的,并且很显然没有明显的答案。Sun 的建议是对于任何情况使用它们,而 C# 方式(也就是 Eckel 和其他人所赞同的)是对于任何情况都不使用它们。其他人说,“还存在一个中间情形。” 通过在 C++ 中使用非常,其中所有的非常都是非检查型的,我已经发现非检查型非常的最大风险之一就是它并没有按照检查型非常采用的方法那样自我文档化。除非 API 的创建者明确地文档化将要抛出的非常,否则调用者没有办法知道在他们的代码中将要捕捉的非常是什么。不幸的是,我的经验是大多数 C++ API 的文档化异常差,并且即使文档化很好的 API 也缺乏关于从一个给定方式可能抛出的非常的足够信息。我看不出有任何理由可以说该问题对于 Java 类库不是同样的常见,因为 Jav 类库严峻依靠于非检查型非常。依靠于您自己的或者您的合作伙伴的编程技巧是异常困难的;假如不得不依靠于某个人的文档化技巧,那么对于他的代码您可能得使用调用栈中的十六个帧来作为您的主要的错误处理机制,这将会是令人恐慌的。 文档化问题进一步强调为什么懒惰是导致选择使用非检查型非常的一个不好的原因,因为对于文档化增加给包的负担,使用非检查型非常应该比使用检查型非常甚至更高(当文档化您所抛出的非检查型非常比检查型非常变得更为重要的时候)。 文档化,文档化,文档化 假如决定使用非检查型非常,您需要彻底地文档化这个选择,包括在 Javadoc 中文档化一个方式可能抛出的所有非检查型非常。Johnson 建议在每个包的基础上选择检查型和非检查型非常。使用非检查型非常时还要记住,即使您并不捕捉任何非常,也可能需要使用 try...finally 块,从而可以执行清除动作例如关闭数据库连接。对于检查型非常,我们有 try...catch 用来提示增加一个 finally 子句。对于非检查型非常,我们则没有这个支撑可以依赖。 返回类别: 教程 上一教程: static与final变量 下一教程: bug? Hbm2JavaTask无法实现joined-subclass单独配置文件(2.1.2) 您可以阅读与"Java 理论与实践: 关于非常的争论"相关的教程: · Java 理论与实践: 关于非常的争论 · Java 理论与实践: Web 层的状态复制 · Java 理论与实践: JDK 5.0 中更灵活、更具可伸缩性的锁定机制 · Java 理论与实践:变还是不变? · Java 理论与实践: 哈希 |
![]() ![]() |
快精灵印艺坊 版权所有 |
首页![]() ![]() ![]() ![]() ![]() ![]() ![]() |