CSDN博客

img wssmao

谈java的异常设计

发表于2008/9/30 8:26:00  730人阅读

摘要

 

Java异常模型的设计是Java应用的架构设计中最重要的决定之一。本文从Java语言规范中第11章对异常的介绍出发,探究Java异常的本质,并介绍Java异常设计的基本原则。

 

异常的本质

 

Java语言规范中对于异常的定义如下:

当程序违反了Java语言的语义约束时,Java虚拟机将使用异常来报告这个错误。

Java语言的设计目标之一是提供可移植性(portability)和健壮性(robustness),为了达到这个目标,当违反语义约束时,Java将抛出一个异常,并且将程序执行的控制流从异常发生的点转移到用户指定的点。

异常也可以被显式地使用throw关键字来抛出。而这种显式抛出的机制被设计用来替代老式的错误处理风格------返回值风格(比如返回-1表示错误发生,返回0表示正常执行)。这种老式错误处理风格的弊端就在于经验表明,由于对返回值的处理不是一个强制性的约束,很多返回值都被调用者所忽视从而使得程序潜藏错误,从而损失了健壮性。所以,异常是报告错误的标准机制

 

抛出异常的时候,Java虚拟机会立马完成当前线程没有完成执行的表达式、语句、方法和构造器调用、初始化器、以及域初始化语句。Java异常机制与它的同步模型是集成的,所以当发生异常的时候,synchronized语句或同步函数调用的锁会释放掉。

 

何时应该抛出异常

 

Java语言规范中给出了三个抛出异常的原因:

  • Java虚拟机同步(synchronized)检测到反常的(abnormal)执行条件,这些条件可能由下列条件触发:
    • 对表达式的求值违反了语言的语义,例如除0等
    • 加载或链接程序某部分的时候发生了错误
    • 超出了某些资源的限制,比如内存不足等
  • throw语句被执行
  • 发生了异步异常:
    • ThreadThreadGroupstop方法被调用
    • 虚拟机发生了内部错误 Java语言规范也明确指出了异常的三种类型:RuntimeExceptionErrorcheckedException(检查型异常)。第一种类型的异常称为非检查型异常(uncheckedException),不需要声明,所以不需要程序员显式地使用try...catch语句来捕获它们;而后面两种则要求声明,即必须使用try...catch来捕获并处理

Error不被检查的原因在于,它们可能发生在程序的任何地方,并且恢复它们非常困难或者不可能。RuntimeException不被检查的原因则在于声明它们并不会有助于建立程序的正确性。

 

异常设计的原则

 

通常,应该严格限制自己只抛出异常(Exception的子类),而非错误。把错误的抛出留给那些大牛们。

 

Sun在其The Java Tutorial中阐明了何时使用检查型异常和非检查型异常。Sun认为检查型异常应该是标准用法,也就是说都应该使用检查型异常,除非你是JVM,才可以抛出RuntimeException。这显得过于严格。

 

而在Effective Java: Programming Language Guide这本书里面,关于异常部分没有Sun那样严格,但还是一致的:

  • Item 39: 仅为异常情况使用异常------即是说不要将异常用于控制流。
  • Item 40: 对可恢复异常情况使用检查型异常;对编程错误使用运行时异常------编程错误类似与前置条件错误等情况。
  • Item 41: 避免使用不必要的检查型异常------换句话说就是不要对调用者根本就不可能恢复的情况或者说可以预见到必然会使程序退出的情况使用检查型异常。
  • Item 43: 抛出异常应与抽象层次相适应------方法抛出的异常必须和该方法所实现的功能在抽象层次上一致。

关于如何使用检查型异常的争论很多。Bruce Eckel(Thinking in Java的作者)和Rod Johnson(J2EE Design and Development的作者)就分别指出了过度使用检查型异常带来的问题:

  • 检查型异常可能会不适当地暴露实现细节------当违反了上面的Item 43时。
  • 检查型异常可能会导致不稳定的方法签名------当改变了方法实现,有可能就会导致异常类型的变化从而导致方法的签名改变。事实上这也是违反Item 43时可能出现的问题。
  • 不可读的代码------如果异常较多,那么try...catch语句的篇幅可能超过正常流代码的篇幅,导致阅读代码困难。
  • 异常淹没------两种情况,一种是空catch语句;另一种是过于一般化的异常类型。这通常是违反Item 40时导致的情况。
  • 过多的异常包装------当严格遵守Item 43时,又可能出现这种情况。例如将一个非常底层的IOException穿越很多层抛出,每一层可能都会对其进行封装。这通常会引起性能问题。同时,在你处理这些异常的时候,也会增加你找出真正原因的困难。

对于上面这些问题,Eckel的解决方案相当激进,他倡导将所有异常都应该是非检查的(C#也是这样做的)。而Johnson则使用相对保守的方法,他将异常分为几个类别,并针对每一种类别确定了一种策略。Johnson认为有些异常基本上就是作为"第二返回值"(通常用于报告对业务逻辑的违反)来使用的,对于这种异常应该使用检查型异常;有些异常则表示程序有严重的错误,这些异常几乎没有人可以很好的处理它,它们将顺着抽象层次一直抛到最上面也无法处理,对于这些异常,Johnson认为应该使用非检查型异常;另外还存在一直介于上面两种异常之间的异常,仅有少数人希望捕获并处理它们,对于这一类异常,Johnson也认为应该使用非检查型异常。


Barry Ruzek则构建了一个Java异常与意外情况和错误的映射表:

异常条件
意外事件
错误
认为是
设计的一部分
难以应付的意外
预期发生
有规律但很少
从不
谁来处理
调用方法的上游代码
需要修复此问题的人员
实例
另一种返回模式
编程缺陷、硬件故障、配置错误、文件丢失、服务器无法使用
最佳映射
检查型异常
非检查型异常

 

同时Ruzek运用分离关注点理论,提出了错误屏障(fault barrier)模式。该模式中,任何应用程序组件都可以抛出错误异常,但是只有作为"错误屏障"的组件可以捕捉到错误异常。这样一来就将错误处理代码和正常的逻辑代码完全分开了。错误屏障靠近调用栈的顶端,其职责包括:首先记录包含在异常中的错误信息,然后以受控方式停止操作。

 

总结

 

根据以上的介绍,可以得出以下结论:

  1. 异常就是意外情况,是违反语义约束的情况,是违反契约的情况。
  2. 异常的本质是一种错误的报告机制。我们不应该使用异常来报告可以作为方法正常功能的情况。
  3. 我们应该在很多情况下严格将自己限制为只抛出异常(Exception)而非错误(Error)。
  4. 如果是客户没有履行契约(前置条件错误),应该抛出非检查型异常。
  5. 只对那些可恢复的情况抛出检查型异常。
  6. 针对作为第二返回值的异常情况使用检查型异常。
  7. 抛出的异常应与功能抽象层次相一致,具体地,当穿越多个抽象层次抛出异常时,应该对异常进行包装,以获得各个抽象层次的独立性。
0 0

相关博文

我的热门文章

img
取 消
img