JAVA异常处理原理

一、Java异常处理机制的优点

在一些传统的编程语言,如C语言中,并没有专门处理异常的机制,程序员通常用方法的特定返回值来表示异常情况,并且程序的正常流程和异常流程都采用同样的流程控制语句。

Java语言按照面向对象的思想来处理异常,使得程序具有更好的可维护性。Java异常处理机制具有一下优点:

(1)把各种不同类型的异常情况进行分类,用Java类来表示异常情况,这种类被称为异常类。把异常情况表示成异常类,可以充分发挥类的可扩展和可重用的优势。

(2)异常流程的代码和正常流程的代码分离,提高了程序的可读性,简化了程序的结构。

(3)可以灵活的处理异常,如果当前方法有能力处理异常,就捕获并处理它,否则只需要抛出异常,由方法调用者来处理它。

方法调用的流程:

Java虚拟机用方法调用栈(method invocation stack)来跟踪每个线程中一系列的方法调用过程。该堆栈保存了每个调用方法的本地信息(比如方法的局部变量)。每个线程都有一个独立的方法调用栈。对于Java应用程序的主线程,堆栈底部是程序的入口方法main()。当一个新方法被调用时,Java虚拟机把描述该方法的栈结构置入栈顶,位于栈顶的方法为正在执行的方法。

当一个方法正常执行完毕,Java虚拟机会从调用栈中弹出该方法的栈结构,然后继续处理前一个方法。如果在执行方法的过程中抛出异常,则Java虚拟机必须找到能捕获该异常的catch代码块。它首先查看当前方法是否存在这样的catch代码块,如果存在,那么就执行该catch代码块;否则,Java虚拟机会从调用栈中弹出该方法的栈结构,继续到前一个方法中查找合适的catch代码块。在回溯过程中,如果Java虚拟机在某个方法中找到了处理该异常的代码块,则该方法的栈结构将成为栈顶元素,程序流程将转到该方法的异常处理代码部分继续执行。当Java虚拟机追溯到调用栈的底部的方法时,如果仍然没有找到处理该异常的代码块,按以下步骤处理。

(1)调用异常对象的printStackTrace()方法,打印来自方法调用栈的异常信息。

(2)如果该线程不是主线程,那么终止这个线程,其他线程继续正常运行。如果该线程是主线程(即方法调用栈的底部为main()方法),那么整个应用程序被终止。

异常处理对性能的影响:

一般来说,在Java程序中使用try···catch语句不会对应用的性能造成很大的影响。仅仅当异常发生时,Java虚拟机需要执行额外的操作,来定位处理异常的代码块,这时会对性能产生负面影响。如果抛出异常的代码块和捕获异常的代码块位于同一个地方,这种印象就会小一些;如果Java虚拟机必须搜索方法调用栈来寻找异常处理代码块,对性能的影响就比较大了。尤其当异常处理代码位于调用栈的底部时,Java虚拟机定位异常处理代码快就需要大量的工作。

因此,不应该使用异常处理机制来控制程序的正常流程,而应该确保仅仅在程序中可能出现异常的地方使用try···catch语句。此外,应该使异常处理代码块位于适当的层次,如果当前方法具备处理某种异常的能力,就尽量自行处理,不要把自己可以处理的异常推给方法调用者去处理。

二、运用Java异常处理机制

1、try···catch语句

在try代码块中抛出异常之后,立即转到catch代码块执行或者退栈到上一层方法处寻找catch代码块。

2、finally语句:任何情况下都必须执行的代码

由于异常会强制中断正常流程,这会使得某些不管在任何情况下都必须执行的步骤被忽略,从而影响程序的健壮性。使用finally语句,不管try代码块中是否出现了异常,都会执行finally代码块。

在某些情况下,把finally的操作放在try···catch语句的后面,这也能保证这个操作被执行。这种情况尽管在某些情况下是可行的,但不值得推荐,以为它有两个缺点:

@把与try代码块相关的操作孤立开来,使程序结构松散,可读性差。

@影响程序的健壮性。假如catch代码块继续抛出异常,就不会执行catch代码块之后的操作。

3、throws子句:声明可能会出现的异常

如果一个方法可能会抛出异常,但没有能力来处理这种异常,可以在方法声明处用throws子句来声明抛出异常。

一个方法可能会出现多种异常,throws子句允许声明抛出多个异常,中间用“,”隔开。

异常声明是接口(概念上的接口)的一部分,在JavaDoc文档中应描述方法可能抛出某种异常的条件。根据异常声明,方法调用者了解到被调用方法可能抛出的异常,从而采取相应的措施:捕获异常,或者声明继续抛出异常。

4、throw语句:抛出异常

throw语句用于抛出异常。

值得注意的是,有throw语句抛出的对象必须是java.lang.Throwable类或者其他子类的实例。

5、异常处理语句的语法规则

异常处理语句主要涉及到try、catch、finally、throw、throws关键字,要正确使用它们,就必须遵守必要的语法规则。

(1)try代码块不能脱离catch代码块或finally代码块而单独存在。try代码块后面至少有一个catch代码块或finally代码块。

(2)try代码块后面可以有零个或多个catch代码块,还可以有零个或至多一个finally代码块。如果catch代码块和finally代码块共存,finally代码块必须在catch代码块后面。

(3)try代码块后面可以只跟finally代码块。

(4)在try代码块中定义的变量的作用域为try代码块,在catch代码块和finally代码块中不能访问该变量。如果希望在catch代码块和finally代码块中访问try中用到的变量,则必须把变量定义在try代码块的外面。

(5)当try代码块后面有多个catch代码块时,Java虚拟机会把实际抛出的异常对象和各个catch代码块声明的异常类型匹配,如果异常对象为某个异常类型或其子类的实例,就执行这个catch代码块,而不会再执行其他的catch代码块。

(6)如果一个方法可能出现受检查异常,要么用try···catch语句捕获,要么用throws子句声明将它抛出,否则会导致编译错误

6、异常流程的运行过程

异常流程有try···catch···finally语句来控制。如果程序中还包含了return和System.exit()语句,就会使流程变得更加复杂。

(1)finally语句不被执行的唯一情况就是先执行了用于终止程序的System.exit()方法。java.lang.System类的静态方法exit()用于终止当前的Java虚拟机进程,Java虚拟机所执行的Java程序也随之终止。另外,当catch语句中也抛出异常的情况下,在程序退栈寻找上一个方法的catch代码块之前,会先执行finally代码块。

(2)return语句用于退出本方法。在执行try或catch代码块中的return语句时,假如有finally代码块,会先执行finally代码块。

(3)finally代码块虽然在return语句之前就被执行(这里是指在return返回之前执行,如果return a+b;那么是先执行了a+b,再执行finally代码块,再返回),但finally代码块不能通过重新给变量赋值的方式改变return语句的返回值。

在这里涉及到return语句的机制,如果return a;a是基本类型的变量,这里可以理解的是传值,a的值赋给了一个不知名的变量,return将这个不知名的变量内容返回,所以finally语句只能更改a的内容,不能更改那个和a的值相同的不知名变量的值,所以return的结果不可以被finally中的代码改变;但是如果a是引用类型的变量,这里就不是传值了而是传的引用,这样不知名的变量和a都指向了同一个对象,我们可以通过引用a来改变这个对象,使得这个不知名变量所引用的对象发生改变,同样也不能改变这个不知名变量的内容,它仍然指向这个对象,我们不可以让它指向其他对象或者变成null,因为我们不知道这个不知名变量的名字。

(4)建议不要在finally代码块中使用return语句,以为它会导致以下两种潜在的错误。

第一种错误是覆盖try或catch代码块的return语句。可以这样理解,在try或者catch中的return在把返回的结果赋给一个不知名的临时变量后,执行finally,如果没有finally里的return语句,接着回来将这个不知名变量的内容返回,如果在finally中出现了return语句,那么这个return语句没有被打断,给另一个不知名变量赋值之后,直接返回了,方法退栈,try或catch里的返回没有别执行,这样的结果就是finally中的return覆盖了try和catch中的return语句。

第二中错误是丢失异常。如果catch代码块中有throw语句抛出异常,由于先执行了finally代码块,又因为finally代码块中有return语句,所以方法退栈,catch代码块中的throw语句就没有被执行。

三、Java异常类

在程序运行中,任何中断正常流程的因素都被认为是异常。按照面向对象思想,Java语言用Java类来描述异常。所有异常类的祖先类为java.lang.Throwable类,它的实例表示具体的异常对象,可以通过throw语句抛出。Throwable类提供了访问异常信息的一些方法,常用的方法包括:

(1)getMessage() 返回String类型的异常信息。

(2)printStackTrace() 打印跟踪方法调用栈而获得的详细异常信息。在程序调试阶段,此方法可用于跟踪错误。

Throwable类有两个直接子类:

(1)Error类——表示紧靠程序本身无法恢复的严重错误,比如内存空间不足,或者Java虚拟机的方法调用栈溢出。在大多数情况下,遇到这样的错误时,建议让程序终止。

(2)Exception类——表示程序本身可以处理的异常。当程序运行时出现这种异常,应该尽可能地处理异常,并且使程序恢复运行,而不应当随意终止程序。

JDK中预定义了一些具体的异常。常见异常类的类框图如下:


下面对一些常见的异常做简要的介绍。

(1)IOException:操作输入流和输出流可能出现的异常。

(2)ArithmeticException:数学异常。如果把整数除以0,就会出现这种异常。

(3)NullPointerException:空指针异常。当引用变量为null时试图访问对象的属性或方法,就会出现这种异常。

(4)IndexOutBoundsException:下标越界异常。它的子类ArrayIndexOutOfBoundsException表示数组下标越界异常。

(5)ClassCastException:类型转换异常。

(6)IllegalArgumentException:非法参数异常。可用来检查方法的参数时候合法。

Exception类还可分为两种:运行时异常和受检查异常。

1、运行时异常

RuntimeException类及其子类都被称为运行时异常,这种异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常时,即使没有用try···catch语句捕获它,也没有用throws子句声明抛出它,还是会编译通过。

2、受检查异常

除了RuntimeException及其子类以外,其他的Exception类及其子类(从上面的JDK类图中看,这里就是指IOException,为很么不直接说是IOException呢?原因在于我们自定义的异常,大多说都是继承自Exception类,按照这个定义我们自定义的异常类都是属于受检查异常的,而且这样的定义很明确,Java虚拟机的判断规则也应该就是这样)都属于受检查异常(Checked Exception)。这种异常的特点是Java编译器会检查它,也就是说当程序中可能出现这类异常时,要么用try···catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。

3、区分运行时异常和受检查异常

受检查异常表示程序可以处理的异常。如果抛出异常的方法本身不能处理它,那么方法调用者应该去处理它,从而使程序恢复运行,不至于终止程序。

运行时异常表示无法让程序恢复运行的异常,导致这种异常的原因通常是由于执行了错误操作。一旦出现了错误操作,建议终止程序,因此Java编译器不检查这种异常。

如果程序代码中有错误,就可能导致运行时异常。

由此可见,运行时异常是尽量该避免的。在程序调试阶段,遇到这种异常时,正确的做法是改进程序的设计和实现方式,修改程序中的错误,从而避免这种异常。捕获它并且使程序恢复运行并不是明智的办法,这主要有两方面的原因:

(1)这种异常一旦发生,损失严重。

(2)即使程序恢复运行,也可能会导致程序的业务逻辑错乱,甚至导致更严重的异常,或者得到错误的运行结果。

总之,对于运行时异常,是由于编程不当引起的,就应该修改代码尽量避免。

4、区分运行时异常和错误

Error类及其子类表示程序本身无法修复的错误,它和运行时异常的相同之处就是:Java编译器不会检查它们,当程序运行时出现它们,都会终止程序。

两者的不同之处是:Error类及其子类表示的错误通常是有Java虚拟机抛出的,在JDK中预定义了一些错误类。在应用程序中,一般不会扩展Error类来创建用户自定义的错误类。而RuntimeException类表示程序代码中的错误,它是可以扩展的,用户可以根据特定的问题领域来创建相关的运行时异常类。

四、用户自定义异常

在特定的问题领域,可以通过扩展Exception类(这时定义的异常是受检查异常)或RuntimeException类(这时定义的异常是运行时异常)来创建自定义的异常。异常类包含了和异常相关的信息,这有助于负责捕获异常的catch代码块正确的分析并处理异常。

这里解释一个现象:JDK里面预定义的异常会自动抛出来,比如:整数除以0,如果不做处理,自己就会抛出来,而用户自定义的异常一般需要我们自己throw出来。

这其实是JAVA虚拟机预定义了一系列的异常类型,但都是基础的溢出、空指针等运行时异常,是非常底层的东西,JDK类库以这些基础的异常做拦截处理,然后抛出他们自定义的异常类,和后面的异常转译类似。???

1、异常转译和异常链

在分层的软件结构中,会存在自上而下的依赖关系,也就是说上层的子系统会访问下层系统的API。当位于上层的子系统不需要关系来自底层的异常的细节时,常见的做法是捕获原始的异常,把它转换为一个新的不同类型的异常,再抛出新的异常,把它转换为一个新的不同类型的异常,再抛出新的异常,这种处理异常的方法称为异常转译。

从面向对象的角度来理解,异常转译使得异常类型与抛出异常的对象的类型位于相同的抽象层。例如:车子运行时会出现故障异常,而职工开车上班会出现迟到异常,车子的故障异常是导致职工的迟到异常的原因,如果员工直接抛出车子的故障异常,意味着车子故障是发生在职工身上的,这显然是不合理的,正确的做法是,将在职工类里发生的车子异常转译为迟到异常。

JDK1.4以上版本中的Throwable类支持异常链机制。所谓异常链就是把原始异常包装为新的异常类,也就是说在新的异常类中封装了原始异常类,这有助于查找产生异常的根本原因。具体的用法就是catch到一个初级的异常之后,包装它生成一个新的异常类(以它为参数new一个新的异常类)再抛出来。

2、处理多样化异常

在实际应用中,优势需要一个方法同时抛出多个异常。例如在用户提交的HTML表单上有多个字段域,业务规则要求每个字段域的值都符合特定规则,如果不符合规则,就抛出相应的异常。

如果应用程序不支持在一个方法中同时抛出多个异常,用户每次都只能看到针对一个字段域的验证错误。当更正了一个错误后,重新提交表单,又收到针对另一个字段域的验证错误,这会令用户很烦恼。

有效的做法是每次当用户提交表单后,都验证所有的字段域,然后向用户显示所有的验证错误信息、不幸的是,在Java方法中一次只能抛出一个异常对象,因此需要开发者自行设计支持多样化异常的异常类。一般的做法是,异常类中设计一个List类型的变量,每个catch里面都向这个List变量里添加异常,在最后判断List变量的长度如果大于零就将这个异常List抛出来。

五、异常处理原则

1、异常只能用于非正常情况

异常只能用于非正常情况,不能用异常来控制程序的正常流程。

用抛出异常的手段来结束正常的循环流程是不可取的:

(1)滥用异常流程会降低程序的性能。

(2)用异常类来表示正常情况,违背了异常处理机制的初衷。

(3)模糊了程序代码的意图,影响可读性。

(4)容易掩盖程序代码中的错误,增加调试的复杂性。

2、为异常提供说明文档

在JavaDoc文档中应该为方法可能抛出的所有异常提供说明文档。无论受检查异常还是运行时异常,都应该通过JavaDoc的@throws标签来描述产生异常的条件。

完整的异常文档可以帮助方法调用者正确的调用方法,提供合理的参数,尽可能地避免异常或者能方便地找到产生异常的原因。

3、尽可能地避免异常

应该尽可能地避免异常,尤其是运行时运行时异常。避免异常通常有两种办法:

(1)许多运行时异常是由于程序代码中的错误引起的,只要修改了程序代码的错误,或者改进了程序的实现方法,就能避免这种错误。

(2)提供状态测试方法。有些异常是由于当对象处于某种状态时,不合适某种操作而造成的。例如当高压锅内的水蒸气的压力很大,突然打开锅盖,会导致爆炸。为了避免这类事故,高压锅应该提供状态测试功能,让使用者在打开锅盖前,能够判断锅内的高压蒸汽是否排放完。在程序上,调用某个方法,可以先用状态测试功能来测试一下,满足条件才调用它,避免出现异常。

4、保持异常的原子性

应该尽力保持异常的原子性。异常的原子性是指当异常发生后,各个对象的状态能够恢复到异常发生前的初始状态,而不至于停留在某个不合理的中间状态。对象的状态是否合理,是由特定问题领域的业务逻辑决定的。

保持异常的原子性有以下办法。

(1)最常见的办法是先检查方法的参数是否有效确保当异常发生时还没有改变对象的初始状态(也就是异常发生之前,检查好各个条件,确保异常不会发生才开始改变对象的状态)。

(2)编写一段恢复代码,由它来解释操作过程中发生的失败,并且使对象状态回滚到初始状态。这种办法不是很常用,主要用于永久性的数据结构,比如数据库的事务回滚机制就采取了这种办法。

(3)在对象的临时拷贝上进行操作,当操作成功后,把临时拷贝中的内容复制到原来的对象的对象中。

5、避免过于庞大的try代码块

try代码块越庞大,出现异常的地方就越多,要分析发生异常的原因就越困难。有效的做法是分割各个可能出现异常的程序段落,把它们分别放在单独的try代码块中,从而分别捕获异常。

6、在catch子句中指定具体的异常类型

有些编程新手喜欢用catch(Exception ex)子句来捕获所有异常。这样是不可取的,理由如下:

(1)俗话说对症下药,对不同的异常通常采取不同的处理方式。

(2)会捕获本应该抛出的运行时异常,掩盖程序中的错误。

7、不要在catch代码块中忽略被捕获的异常

只要异常发生,就意味着某些地方出了问题,catch代码块既然捕获了这种异常,就应该提供处理异常的措施,比如:

(1)处理异常。针对该异常采取一些行动,比如弥补异常造成的损失或者给出警告信息等。

(2)重新抛出异常。catch代码块在分析了异常之后,认为自己不能处理它,重新抛出异常。

(3)进行异常转译。把原始异常包装为适合于当前抽象层的另一种异常,再将其抛出。

(4)假如在catch代码块中不能采取任何措施,那就不要捕获异常,而是用throws子句声明异常抛出。

不采取任何操作或者仅仅打印异常信息是不可取的。在catch代码块中调用异常类的printStackTrack()方法对调试程序有帮助,但程序调试阶段结束之后,printStackTrack()方法就不应该在异常处理代码块中负担主要责任,因为光靠打印信息并不能解决实际存在的问题。

你可能感兴趣的:(JAVA)