扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
为什么异常非常重要
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设计人员的思考方式,即他们认为大多数失败情况都相同,并且可以用相同类型的异常来传达。
错误和意外事件
假想金融应用软件中的CheckingAccount类。CheckingAccount属于客户,维护当前余额,并且可以接受存款,根据支票接受终止支付命令,以及处理入帐的支票。CheckingAccount对象必须协调并发线程的访问,因为每一个线程都可以改变它的状态。CheckingAccount的processCheck()方法将Check对象作为参数,从账户余额中正常扣除支票金额。但是调用processCheck()的支票结算客户端必须为两类意外事件做好准备。首先,CheckingAccount 可能有为支票注册的终止支付命令。第二,账户中可能没有足够的资金来支付支票金额。
所以,processCheck()方法使用三种可能的方式来响应其调用者。正常的响应方式是支票得到处理,在方法签名中声明的结果返回给调用服务。这两类意外事件响应代表了金融领域非常真实的情况,它们需要与支票结算客户端进行通信。所有这三种processCheck()响应都是为模仿典型的支票账户行为而精心设计的。
在Java中表示意外事件响应的通常方法是定义两种异常,即StopPaymentException和InsufficientFundsException。客户端忽略这两个异常是不正确的,因为在应用程序正常操作时会被抛出这两个异常。这两个异常有助于表达方法的所有行为,和方法签名一样十分重要。
客户端可以轻松地处理这两类异常。如果终止支票的支付,客户端可以取得支票进行特殊处理。如果没有足够的资金,为支付此支票,客户端将从客户的储蓄帐户中转移资金,并重新尝试。
这些意外事件可以预见,它是使用CheckingAccount API的自然结果。它们并不表示软件或执行环境的失败。将这些异常条件与实际的失败对比,实际的失败是由于CheckingAccount类的内部实现细节问题造成的。
假设CheckingAccount在数据库中维持持久的状态并使用JDBC API进行访问。在该API中,几乎每一个数据库访问方法都有失败的可能性,但原因与CheckingAccount实现无关。例如,有人会忘记打开数据库服务器、不小心拔下了网线,或改变了访问数据库的密码。
JDBC依靠单独的已检查异常SQLException来报告一切可能的错误。大多数错误都与数据库的配置、连接和硬件设备有关。processCheck()方法并不能以有意义的方式处理这些异常条件。很遗憾,因为processCheck()至少知道它自己的实现方式。调用堆栈中的上游方法能够处理这些问题的可能性会更小。
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者