异常淹没 我们都看到过这样的代码,其中捕获了一个异常,但是在 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 子句。对于非检查型异常,我们则没有这个支撑可以依靠。  
说明:本教程来源互联网或网友上传或出版商,仅为学习研究或媒体推广,wanshiok.com不保证资料的完整性。
2/2 首页 上一页 1 2 |