Java异常
“好的程序设计语言能够帮助程序员写出好程序,但是无论哪种语言都避免不了程序员写出坏的程序。”
----《Java编程思想》
“充分发挥异常的优点,可以提高程序的可读性、可靠性和可维护性。如果使用不当,它会带来诸多的负面影响,甚至使程序编写无法继续。”
----《EffectiveJava》
异常是一把双刃剑,而且更多的时候会带来负面的影响。在编程中,程序员很多的时候不愿意去处理异常情况或者那些异常发生的时候已经对程序造成了不可挽回的错误,异常处理可能已经不能解决这个问题了,另外在业务处理中添加这异常处理代码,不可避免的影响了这个类本身的意义,降低了程序的可读性和可维护性。
Java提供了可检查异常,目前还没有其他的编程语言采用此机制,而这种机制带来的更多的是不必要的麻烦,但考虑到诸多的原因,这一机制在未来被去除的可能性十分渺茫,如何才能更加优雅的在编程中使用异常确实是一个值得探讨的问题。
首先来看下Java异常的整体关系:
Java的所有异常都是继承自Throwable(这么看都感觉这是个接口),异常分为两大类:普通异常和错误。
普通异常分为检查异常和非检查异常,所谓检查异常就是“在编译器强制检查”,平时编程的时候强制出现的异常就是检查异常,而非检查异常“又叫运行时异常,在运行的时候出现的异常”,比如空指针或者数组越界等等都归为此类。
错误基本就是由于出现了不可挽回的错误从而导致整个程序的失败,这里就不作描述了,因为我们无能为力。
1.捕获异常和抛出异常
这是对异常处理的两种方式,何时抛出?何时捕获?举一个简单的例子:除数是有可能为0的,所以进行检查时很有必要的。但除数为0代表的意义究竟是什么?通过当前正在解决问题的环境,或许能够清楚如何处理除数为0的情况。但是如果不能,就应该抛出异常。这是一个简单通用的原则,但并不是所有情况都适合。异常抛出之后会在堆中创建一个异常对象,从当前环境中弹出异常对象的引用,同时异常处理机制接管程序,并寻找一个恰当的位置来继续执行程序,这个恰当的位置便是异常处理程序。
由于抛出和捕获使得我们可以把业务处理当作一个事务来进行,而异常可以看作是维护这些事务的底线,一旦事务出现问题,那么异常就有可能可以保障事务“恢复”到某个稳定点上。所以如果对出现的异常有“恢复”能力,那么捕获异常就是不错的选择。
2.检查异常和未受检查异常
对于可恢复的情况适用受检查异常,对于编程错误适用未受检查异常。选择决定使用哪种异常的主要原则是:如果期望调用者能够适当地恢复,对于这种情况的就应该使用受检查的异常。因为它会强迫调用者在catch子句中处理该异常或者是将其传播出去。因此,方法中声明要抛出的每个受检查的异常,都是对API用户的一种潜在提示:这个异常是有可能被挽回的。
未受检查异常往往标识编程错误,如果程序抛出未受检查异常,继续执行下去有益无害。如果程序没有捕捉到这样的可抛出结结构,将会导致当前线程停止,并出现适当的错误消息。由于Error往往被JVM保留用于表示资源不足、约束失败,或者其他无法继续执行的条件。而且这个已经是被普遍接受的管理,所以一般使用运行时异常来表示编程错误。
大多数运行时异常都表示前提违例,即API调用者没有遵守API所建立的约定,比如传入了空值、数组访问越界等等,因此,你实现的所有未受检查的抛出结构应该是RuntimeException的子类。
总而言之,对于可恢复的情况适用受检查异常;对于程序错误,则适用运行时异常。当然,黑白并不是那么分明,具体的编程应该具体分析,比如造成运行时异常有可能是由于编程错误引起的,是临时的错误,那么API设计者就需要考虑资源的枯竭是否允许恢复。
3.更好的捕获异常
如果方法抛出的异常与它所执行的任务没有明显的联系,高层的处理需要此信息却并不是和此方法直接关联,这在分层的系统中经常出现。对于此类的问题,可以一层一层的抛出,但是这样不仅仅会让层次之间的耦合变高,而且还有可能由于底层的实现细节从而污染更高层的API。所以,一个更加好的方式是中间层捕获异常后抛出对高层处理更加适合的异常,即抛出于抽象对应的异常,这种做法被称为-----异常转义。从这里也可以看出,虽然捕获了异常,但是实际却没有去实现处理异常的细节。
上面更好的做法也会导致一个问题----异常丢失,由于是抛出了自己新的对高层更加合适的异常,底层的异常便对高层不可见,这对问题的发现(特别是调试)是不利的。所以,高层应该同时能够捕获底层的异常,在JDK1.4以前,是使用编程的方式来实现这种需求,现在有更加适合的技术-----异常链。
4.更好的抛出异常
抛出的异常对象中应该在细节消息中包含详细的失败信息。在异常出现的时候,可以打印堆栈信息,此方法可以得到和失败的一些消息,但是那个只是针对程序员的调试信息,在业务中处理异常的时候需要更加详细和适用的失败信息。异常的细节信息应该包含对处理该异常有贡献的参数和域的值,对异常处理的方法可以通过获取到这些信息。虽然目前Java平台还没有对此特定的支持,但是利用编程(比如使用getMessage())方法来得到相关的失败信息。
提供这样的信息或者访问方法,相对于不受检查异常,对受检查异常更加的重要,因为受检查的异常一般是想从错误中恢复,所以提供细节信息是相当有必要的。不受检查异常想要获得这些信息一般是少见的(如果你遵守了前面的编程规则)。
5.失败原子性
当由于失败抛出异常后,通常期望这个对象仍然保持在一种定义良好的状态之中,即使失败是发生在执行过程的中间。对于受检查异常而言,这显得尤为重要,因为调用者有可能期望能够从这种异常中恢复。一般而言,失败的方法调用应该使对象保持在被调用之前的状态。具有这种属性的方法被称为具有失败原子性。
这种思想与事务类似,具体可以实现的途径有以下四种:
1.final化属性
2.调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态呗修改之前发生。(由于各种原因,这个在稍微复杂的编程中基本不能实现)
3.编写一段恢复代码。
4.临时拷贝对象,完成处理后在复制值到原始对象。
实际的编程中很有可能还会考虑同步问题,所以,实现失败原子性其实并不是如人们所期望的那样。所以,一般而言,作为方法规范的一部分,产生的任何异常都应该让对象保持在之前的状态。如果违法这条规则,API文档就应该清楚的指明对象将会处于什么状态。
6.每个方法抛出的异常都要有文档
异常的抛出意味着程序出现了问题,想要使程序恢复执行,需要格外的用心。所以,为每个方法抛出的异常编写详细的文档是十分有必要的。
前提条件、状态变化、异常信息等等都应该详细的记录,抛出异常的方法应该使用Javadoc的@throws标记。
这一块不要怕花时间,文档编写的价值是相当的高的。
7.优先使用标准异常
虽然自定义异常更加的灵活,但是有可能使得代码的阅读性变差,很明了,阅读的人多了一道理解你定义的异常的步骤。但是,标准异常并不总是那么精确。使用自定义异常还是标准异常完全取决你的问题。
8.其它的一些建议
1.只针对异常的情况才使用异常。
正常的控制流程中应该避免使用异常来解决问题。比如如下代码
Try{
IntI = 0;
While(true){
Range[i++].climb();
}
}catch(ArrayIndexOutOfBoundsExceptione){
}
很明显,相对于foreach语法,这种方式不但显得累赘,而且其效率还十分的底下, 而对于数组越界这种运行时异常,是不可恢复异常,属于编程错误(上面所述),应该 抛出而不是捕获。
2.不要忽略异常
有时候由于不想处理异常,从而将catch后面的代码块置空,这样的做法就相当于 一个定时炸弹,而且当它爆炸的时候你根本不知道是它炸的!
3.使用详细的异常
异常匹配不会像switch那样精确,使用详细的异常将是你日后的一笔财富!
9.闲话
错误在编译的时候发现时最好的发现时间,一旦运行起来,就属于运行时错误。Java的异常机制是对使用C语言那些约定俗成的编程注意事项的包装。而运行时异常往往是无法挽回的,对于Java的异常到底对Java编程有多大好处还是相反,这个要辩证的看待,确实,很多时候即使是那些检查异常也无法挽回,失败原子性的要求就更加的高。
异常的处理和业务处理往往其关联程度是不大的,如果放在一个类中进行处理,对于以后来说可能将会是nightmare!所以,将异常处理和业务处理分离是义不容辞的责任,具体的如何处理可以参考Spring的异常处理方式。
对于目前的我来说,异常最大的好处就是“报告”,在程序开发的时候会带来许多的方便。Java的异常就像其泛型一样备受争议,到底如何优雅的使用异常是本文讨论的一个话题,但是,具体的如何写出优雅的异常,还是具体问题具体分析和经验的问题,这里,所能做的,只能给你提供前人所总结的经验。