十二、通过异常处理错误
Java的基本理念是“结构不佳的代码不能运行”。
Java中的异常处理的目的在于通过使用少于目前数量的代码来简化大型、可靠的程序的生成,并且通过这种方式可以使你更加自信:你的程序中没有未处理的错误。
1.概念
C以及其他早期语言常常具有多种错误处理模式,这些模式往往建立在约定俗成的基础上,而并不属于语言的一部分。通常会返回某个特殊值或者设置某个标志,并且假定接收者将对这个返回值或标志进行检查,以判定是否发生了错误。然而,对于构造大型、健壮、可维护的程序而言,这种错误处理模式已经成为了主要障碍。
解决的办法是,用强制规定的形式来消除错误处理过程中随心所欲的因素。这种做法由来已久,对异常处理的实现可以追溯到20世纪60年代的操作系统,甚至于BASIC语言中的on error goto语句。而C++的异常处理机制基于Ada,Java中的异常处理则建立在C++的基础之上。
使用异常所带来的一个相当明显的好处是,它往往能够降低错误处理代码的复杂度。如果不使用异常,那么就必须检查特定的错误,并在程序中的许多地方去处理它。而如果使用异常,那就不必在方法调用处进行检查,因为异常机制将保证能够捕获这个错误。并且,只需在一个地方处理错误,即所谓的异常处理程序中。这种方式不仅节省代码,而且把“描述在正常执行过程中做什么事”的代码和“出了问题怎么办”的代码相分离。
2.基本异常
异常情形是指阻止当前方法或作用域继续执行的问题。把异常情形与普通问题相区分很重要,所谓的普通问题是指,在当前环境下能得到足够的信息,总能处理这个错误。而对于异常情形,就不能继续下去了,因为在当前环境下无法获得必要的信息来解决问题。你所能做的就是从当前问题跳出,并且把问题提交给上一级环境。这就是抛出异常时所发生的事情。
当抛出异常后,有几件事会随之发生。首先,同Java中其他对象的创建一样,将使用new在堆上创建异常对象。然后,当前的执行路径将终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是异常处理程序,它的任务是将程序从错误状态中恢复,以使程序能要么换一种方式运行,要么继续运行下去。
①异常参数
所有标准异常类都有两个构造器:一个是默认构造器;另一个是接受字符串作为参数,以便能把相关信息放入异常对象的构造器。
通常,异常对象中仅有的信息就是异常类型,除此之外不包含任何有意义的内容。
3.捕获异常
要明白异常是如何被捕获的,必须首先理解监控区域的概念。它是一段可能产生异常的代码,并且后面跟着处理这些异常的代码。
①try块
如果在方法内部抛出了异常,这个方法将在抛出异常的过程中结束。要是不希望方法就此结束,可以在方法内设置一个特殊的块来捕获异常。因为在这个块里“尝试”各种方法调用,所以称为try块。
try { //Here's the code }
②异常处理程序
异常处理程序紧跟在try块后面,以关键字catch表示。
4.创建自定义异常
要自己定义异常类,必须从已有的异常类继承,最好是选择意思相近的异常类。
对异常来说,最重要的部分就是类名。
5.异常说明
异常说明属于方法声明的一部分,紧跟在形式参数列表之后。
6.捕获所有的异常
可以只写一个异常处理程序来捕获所有类型的异常。通过捕获异常类型的基类Exception就可以做到这一点。
printStackTrace()方法所提供的信息可以通过getStackTrace()方法来直接访问,这个方法将返回一个由栈轨迹中的元素所构成的数组,其中每一个元素都表示栈中的一帧。元素0是栈顶元素,并且是调用序列中的最后一个方法调用。
异常链
常常会想要在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下去,这被称为异常链。现在所有的Throwable的子类在构造器中都可以接受一个cause对象作为参数。这个cause就用来表示原始异常,这样通过把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,也能通过这个异常链追踪到异常最初发生的位置。
只有三种基本的异常类提供了带cause参数的构造器,分别是Error、Exception和RuntimeException。如果要把其他类型的异常链接起来,应该使用initCause()方法而不是构造器。
7.Java标准异常
Throwable这个类被用来表示任何可以作为异常被抛出的类。Throwable对象可分为两种类型:Error用来表示编译时和系统错误;Exception是可以被抛出的基本类型。Java程序员关心的基类型通常是Exception。
异常的基本概念是用名称代表发生的问题,并且异常的名称应该可以望文知意。
①特例:RuntimeException
运行时异常会自动被Java虚拟机抛出,被称为“不受检查异常”。
如果RuntimeException没有被捕获而直达main(),那么在程序退出前将调用异常的printStackTrace()方法。
RuntimeException代表的是编程错误:
1)无法预料的错误。例如null引用
2)作为程序员,应该在代码中进行检查的错误。
8.使用finally进行清理
无论try块中的异常是否被抛出,finally子句里的代码总能运行。
当要把除内存之外的资源恢复到它们的初始状态时,就要用到finally子句。这种需要清理的资源包括:已经打开的文件或网络连接,在屏幕上画的图形,甚至可以是外部世界的某个开关。
当涉及break和continue语句的时候,finally子句也会得到执行。
当用某些特殊的方式使用finally子句,会出现异常丢失的情况。例如在finally子句中return。
9.异常的限制
当覆盖方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。这意味着,当基类使用的代码应用到其派生类对象的时候,一样能够工作,异常也不例外。
异常限制对构造器不起作用。但派生类构造器的异常说明必须包含基类构造器的异常说明。派生类构造器不能捕获基类构造器抛出的异常。
不能基于异常说明来重载方法。一个出现在基类方法的异常说明中的异常,不一定会出现在派生类方法的异常说明中。
10.构造器
对于在构造阶段可能会抛出异常,并且要求清理的类,最安全的使用方式是使用嵌套的try子句。
11.异常匹配
抛出异常的时候,异常处理系统会按照代码的书写顺序找出“最近”的处理程序。找到匹配的处理程序之后,它就认为异常将得到处理,查找不再继续。
派生类的对象也可以匹配其基类的处理程序。
12.异常使用指南
1)在恰当的级别处理问题。
2)解决问题并且重新调用产生异常的方法。
3)进行少许修补,然后绕过异常发生的地方继续执行。
4)用别的数据进行计算,以代替方法预计会返回的值。
5)把当前运行环境下能做的事情尽量做完,然后把相同或不同的异常抛到更高层。
6)终止程序。
7)进行简化。
8)让类库和程序更安全。
13.总结 以上简略了许多内容,许多更复杂一些的问题不是一言两语。
异常可以帮助我们制造出更多健壮的程序,是相当重要又不那么复杂的知识。应该熟练掌握