Java开发人员做出的有关架构的重要的决定之一便是如何使用Java异常模型。Java异常处理成为社区中讨论多的话题之一。一些人认为Java语言中的已检查异常(Checked Exceptions)是一次失败的尝试。本文认为错误并不在于Java模型本身,而在于Java库设计人员没有认识到方法失败的两个基本原因。本文提倡 思考异常情况的本质,并描述了有助于用户设计的设计模式。后,本文讨论了异常处理在面向方面编程(Aspect Oriented Programming)模型中作为横切关注点(crosscutting concern)的情况。如果使用得当,Java异常将对程序开发人员大有裨益。本文将帮助读者正确使用Java异常。

  为什么异常非常重要

  Java应用程序中的异常处理可以告诉用户构建应用程序的架构强度。架构是指在应用程序的各个层面上所做出的并始终遵守的决策。其中重要的决 策之一便是应用程序中类、子系统或层之间进行互相通信的方式。方法通过Java异常可以为操作传递另一种结果,因此应用程序架构特别值得我们去关注。

  判断Java架构师技能的高低和开发团队是否训练有素,其中比较好的方法是查看应用程序中的异常处理代码。首先需要观察的是有多少代码专门用于 捕捉异常、记录异常、确定发生的事件和异常转化。简洁、紧凑和有条理的异常处理表明团队有使用Java异常的一致方法。当异常处理代码的数量将要超过其他 方面的代码时,可以断定团队成员之间的沟通已经打破(或者这种沟通从一开始不存在),每个人都用自己的方法来处理异常。

  临时异常处理的结果非常具有预见性。如果问团队成员为什么在代码的某个特定点丢弃、捕捉、或忽略某个异常,回答通常是:“除此之外,我不知道怎 么做。”如果问他们在编写代码的异常实际发生时会产生什么情况,他们通常会皱眉,回答类似于:“我不知道。我们从来没有测试过。”

  要判断Java组件是否有效地利用了Java异常,可以查看其客户端的代码。如果客户端代码中包含大量计算操作失败时间、原因和处理方法的逻 辑,原因几乎都是由于组件的错误报告设计。有缺陷的报告会在客户端产生大量的“记录和遗留”(log and forget)代码,而没有任何用途。糟糕的是扭曲的逻辑路径、互相嵌套的try/catch/finally程序块,以及其他导致脆弱和无法管理的应 用程序的混乱。

  事后处理异常(或者根本不处理)是造成软件项目混乱和延迟的主要原因。异常处理关系到软件设计的各个方面。为异常建立架构约定应该是项目中首先要做出的决定之一。合理使用Java异常模型将对保持应用程序的简洁性、可维护性和正确性大有帮助。

  挑战异常准则

  如何正确使用Java异常模型已经成为大量讨论的主题。Java并不是支持异常语义的第一种语言;但是,通过Java编译器可强制使用规则来控 制如何声明和处理特定的异常。许多人认为编译时异常检查对精确软件设计有帮助,它与其他语言特征能够很好地协调起来。图1表明了Java异常的层次结构。

  通常,Java编译器会根据java.lang.Throwable强制方法抛出异常,包括其声明中“throw”子句的异常。同样,编译器会 验证方法的客户端是捕获声明异常类型还是指定自己抛出异常类型。这些简单的规则对于全世界的Java开发人员产生了深远的影响。

  编译器针对Throwable继承树的两个分支放宽了异常检查行为。java.lang.Error和 java.lang.RuntimeException的子类免于编译时检查。在两者中,软件设计人员通常对运行时异常更感兴趣。通常使用术语“未检查异 常”(unchecked exception)来区分其他的所有“已检查异常”(checked exception)

图 1 Java异常层次结构

  我认为已检查异常会受到那些重视Java语言中强类型的人的欢迎。毕竟,由编译器对数据类型产生的约束会鼓励严格编码和精确思维。编译时类型检 查有助于防止在运行时产生难以应付的意外事件。编译时异常检查将起到相同的效果,提醒开发人员注意的是,方法会有潜在的其他结果需要进行处理。

  在早期,推荐无论何处都尽量使用已检查异常,以便充分借助编译器生产出无差错软件。Java API库的设计人员显然赞成已检查异常准则,并广泛使用这些异常来模仿在库方法中发生的任何意外事件。在J2SE 5.1 API规范中,已检查异常类型仍然比未检查异常类型多,比例要超过二比一。

  对于程序员来说,Java类库中的绝大多数公共方法好像为每一个可能的失败都声明了已检查异常。例如,java.io包对已检查异常IOException的依赖性特别大。至少63个Java库包直接或间接通过其数十个子类之一发布了此异常。

  I/O失败很严重但也很少见。另外,程序代码通常没有能力从失败中恢复。Java程序员发现他们必须提供IOException和类似在简单的 Java库方法调用时可能发生的不可恢复的事件。捕捉这些异常只会打乱原有简洁的代码,因为捕捉块并不能改善此类情况。而不捕捉这些异常可能会更糟糕,因 为编译器要求将这些异常加入到方法所抛出的异常列表中。这公开了一些实现细节,的面向对象设计自然会将这些细节隐藏起来。

  这种无法成功的情况导致许多严重违反Java编程模式的异常处理。当今的程序员经常被告诫要提防这些情况。同样,在创建工作区方面也产生大量正确和错误的建议。

  一些Java天才开始质疑Java已检查异常模型是否是一次失败的尝试。可以确定某个地方出了问题,但是这和Java语言中的异常检查无关。失败的原因是Java API设计人员的思考方式,即他们认为大多数失败情况都相同,并且可以用相同类型的异常来传达。