第九章 异常处理
异常情况会改变正常的流程,导致恶劣的后果,为了减少损失,应该事先充分预料所有可能出现的异常,然后采取以下措施:
首先考虑避免异常,彻底杜绝异常的发生;如果不能完全避免,则尽可能地减少异常的发生的几率。
如果有些异常不可避免,那么应该预先准备好处理异常的措施,从而降低或弥补异常造成的损失,或者恢复正常的流程。
对于某个系统遇到的异常,有些异常单靠系统本身就能处理,有些异常需要系统本身及其它系统共同处理。
对于某个系统遇到的异常,系统本身应该尽可能地处理异常,实在没办法处理,才求助于其它系统处理。
Java系统提供了一套完善的异常处理机制。正确的运用这套机制,有助于提高程序的健壮性。所谓健壮性是指程序在多数情况下能够正常运行,返回预期的结果,如果偶尔遇到异常情况,程序也能采取周到的解救措施。
9.1 Java异常处理机制概述
在处理异常中要考虑的两个问题:
如何表示异常情况
如何控制处理异常的流程
9.1.1 Java异常处理机制的优点
传统的异常处理方式尽管是有效的,但存在以下缺点:
表示异常情况的能力有限,单靠方法的返回值难以表达异常情况包含的所有信息。
异常流程的代码和正常流程的代码混合在一起,影响程序的可读性,容易增加程序结构的复杂性。
随着系统规模的不断扩大,这种处理方式已经成为创建大型可维护项目的障碍。
Java语言按照面向对象的思想来处理异常,使得程序具有更好的可维护性。Java异常处理机制具有以下优点:
把各种不同类型的异常情况进行分类,用Java类表示异常情况,这种类被称为异常类。把异常情况标识成异常类,可以充分发挥类的可扩展性和可重用的优势。
异常流程的代码和正常流程的代码分离,提高了程序的可读性。简化了程序的结构。
可以灵活地处理异常,如果当前方法有能力处理异常,就捕获并处理它,否则只需抛出异常,由方法调用者来处理。
9.1.2 Java虚拟机的方法调用栈
Java虚拟机使用方法调用栈来跟踪每个线程种一系列的方法调用过程。该堆栈中保存了每个调用方法的本地信息(比如方法的局部变量)。每个线程都有独立的方法调用栈。对于方法Java应用程序的主线程,堆栈底部是程序的入口方法main(),当一个方法被调用时,Java虚拟机把描述该方法的栈结构置入栈顶部,位于栈顶的方法为正在执行的方法。
如果方法中的代码块可能抛出异常,有两种处理方法:
在当前方法中通过try-catch语句捕获并处理异常。
在方法的声明处通过throws语句声明抛出异常。
当一个方法正常执行完毕后,Java虚拟机会从调用栈中弹出该方法的栈结构。然后继续处理前一个方法,如果在执行方法的过程中抛出异常,Java虚拟机必须找到能够捕获该异常的catch代码块。(首先查看当前方法中是否存在这样的catch代码块,如果存在就执行该语句,否则,Java虚拟机会从调用栈中弹出该方法,继续到前一个方法中寻找合适的catch代码块。)
当Java虚拟机追溯到调用栈的最底部,如果仍然没有找到处理该异常的代码块,将按照以下步骤执行:
调用异常对象的printStackTrace()方法,打印来自方法调用栈的异常信息。
如果该线程不是主线程,那么终止这个线程,其它线程继续正常运行。如果该线程是主线程(即方法调用栈的最底部是main()方法),那么整个应用程序会被终止。
9.1.3 异常处理对性能的影响
一般来说,在Java程序中使用try-catch语句不会对应用的性能造成很大的影响。仅当异常发生时,Java虚拟机需要进行额外的操作,来定位处理异常的代码块,此时会对性能产生负面影响。如果抛出异常的代码块和捕获异常的代码块位于同一个方法中,那么这种影响要小一些;如果Java虚拟机必须搜索方法的调用栈来寻找异常处理代码块,对性能的影响就比较大了。
所以,应该确保仅仅在程序中可能出现异常的地方使用try-catch语句,此外,应该使异常处理代码块位于适当的层次。如果当前方法具备某种处理异常的能力,就尽量自行处理,不要把自己可以处理的异常推给方法的调用者去处理。
9.2 运用Java异常处理机制
9.2.1 try-catch语句
在Java语言中,用try-catch语句进行异常处理:
try{
可能会出现异常情况的代码
}catch (SQLException e){
处理操作数据库出现的异常
}catch(IOException e){
处理操作输入输出流出现的异常
}
9.2.2 finally语句:任何情况下必须执行的代码
finally代码块能够保证特定的操作总是会执行。它的形式如下:
public void work() throws LeaveEarlyException{
try{
开门
工作8小时
}catch(DiseaseException e){
throw new LeaveEarlyException();
}finally{
关门
}
}
不管try代码块中是否出现异常,都会执行finally代码块。
这种处理方式在某些情况下是可行的,但不值得推荐,因为它有两个缺点:
把与try代码块相关的操作孤立开来,使程序结构松散,可读性差。
影响程序的健壮性。
9.2.3 throws子句:声明可能会出现的异常
如果一个方法可能会出现异常,但没有能力处理这种异常,可以在方法的声明处用throws子句来声明抛出异常。
一个方法可能出现多种异常,throws子句允许声明抛出多个异常,例如:
public void method() throws SQLException, IOException{... ...}
异常声明是接口的一部分,在JavaDoc文档中应描述方法可能抛出的异常。
9.2.4 throw语句:抛出异常
throw语句用于抛出异常,有throw语句抛出的对象必须是java.lang.Throwable类或者其子类的实例。
9.2.5 异常处理语句的语法规则
try代码块后面可以有零个或多个catch代码块,还可以有零个或至多一个finally代码块。如果catck代码块和finally代码块同时存在,finally代码块必须在catch代码块后面。
try代码块后面可以只跟finally代码块。
在try代码块中定义的变量的作用域为try代码块,在catch代码块和finally代码块中不能访问该变量。
当try代码块后面有多个catch代码块时,Java虚拟机会把实际抛出异常对象依次和各个catch代码块声明的异常类型匹配,如果异常对象为某个异常类型或其子类的实例,就执行这个代码块,不会再执行其它的catch代码块。
为了简化编程,从JDK7开始,允许在一个catch子句中同时捕获多个不同类型的异常,用|符合进行分隔。
如果一个方法可能出现受检查异常,要么用try-catch语句捕获,要么用throws子句声明将它抛出,否则会导致编译错误。
判断一个方法可能会出现异常的依据如下:
方法中有throw语句。
调用了其它方法,其它方法用throws子句声明抛出某种异常。
针对前一条语法规则,从JDK7开始,如果在catch子句中捕获的异常被声明为final类型,那么当catch子句中继续抛出该异常时,可以不用在定义方法时用throws子句声明将它抛出。
throw语句后面不允许紧跟其它语句,因为这些语句永远不会被执行。
9.2.6 异常流程的运行过程
finally语句不被执行的唯一情况是:先执行了用于终止程序的System.exit()方法。java.lang.System类的静态方法exit()用于终止当前Java虚拟机进程,Java虚拟机所执行的Java程序也会随之停止。exit()方法的定义如下:
public static void exit(int status)
return 语句用于退出本方法。在执行try或catch代码块中的return语句时,加入有finally代码块,会先执行finally代码块。
funally代码块虽然在return语句之前被执行,但finally代码块不能通过重新该变量赋值的方式来改变return语句的返回值。
建议不要在finally代码块中使用return语句,因为他会导致以下两种潜在的错误:
覆盖try或catch代码块中的return语句。
丢失异常。
9.2.7 跟踪丢失的异常
在JDK7中,Throwable接口中增加了两个已经提供的默认实现的方法
public final void addSuppressed(Throwable exception)
public final Throwable[] getSuppressed()
以上add方法把差点丢失的异常保存起来,get方法返回所保存下来的差点丢失的异常。
9.3 Java异常类
Java中,所有异常类的祖先类为java.lang.Throwable类。它的实例表示具体的异常类型,可以使用throw语句抛出。Throwable类提供了访问异常信息的一些方法,常用的方法如下:
getMessage():返回String类型的异常信息
printStackTrace():打印跟踪方法调用栈而获得的详细异常信息。
Throwable类有两个直接子类:
Error类:表示单靠程序本身无法恢复的严重错误,比如内存空间不足,或者Java虚拟机的方法调用栈溢出。在大多数情况下,遇到这种错误时,建议让程序终止。
Exception类:表示程序本身可以处理的异常,当程序运行时出现这类异常,应该尽可能地处理异常,并且使程序恢复运行,而不应该随意终止程序。
JDK中定义了一些具体的异常:
IOException:操作输入流和输出流是可能出现的异常。
ArithmeticException:数学异常。如把整数除以0,就会出现这种异常。
NullPointerException:空指针异常。当引用变量为null时,试图访问对象的属性或方法,就会出现这种异常。
IndexOutOfBoundsException:下标越界异常。它的子类ArrayIndexOutOfBoundsException表示数组下标越界异常。
ClassCastException:类型转换异常。
IllegalArgumentException:非法参数异常,可用来检查方法的参数是否合法。
9.3.1 运行时异常
RuntimeException类及其子类都被称为运行时异常,这种异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常时,即使没有使用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。
由于程序代码不会处理运行时异常,因此当程序在运行时出现了这种异常时,就会导致程序异常终止。
9.3.2 受检查时异常
除了RuntimeException及其子类以为,其它的Exception类及其子类都属于受检查异常。这种异常的特点是Java虚拟机会检查它,也就是说,当程序中可能出现这种异常时,要么使用try-catch语句捕获它,要么使用throws子句声明抛出它,否则编译不会通过。
9.3.3 区分运行时异常和受检查异常
受检查异常表示程序可以处理的异常,如果抛出异常的方法本身不能处理它,那么方法的调用者应该可以处理它,从而使程序恢复运行,不至于终止程序。
运行时异常表示无法让程序恢复运行的异常,导致这种异常的原因通常是执行了错误操作。一旦出现了错误操作,建议终止程序,因此Java虚拟机不检查这种错误。
9.4 用户定义异常
在特定的问题领域,可以通过扩展Exception类或RuntimeException类来创建自定义的异常,异常类包含和异常相关的信息。
9.4.1 异常转译和异常链
9.4.2 处理多样化异常
9.5 异常处理原则
9.5.1 异常只能用于非正常情况
这种处理方式有以下缺点:
滥用异常流程会降低程序的性能
用异常类来表示正常情况,违背了异常处理机制的初衷。
模糊了程序代码的意图,影响可读性。
容易掩饰程序代码中的错误,增加调试的复杂性。
9.5.2 为异常提供说明文档
9.5.3 尽可能地避免异常
许多运行时异常是由于程序代码中的错误引起的,只有修改了程序代码的错误,或者改进了程序的实现方式,就能避免这种错误。
提供状态测试方法。有些异常是由于当对象处于某种状态下,不适合某种操作造成的。
9.5.4 保持异常的原子性
异常的原子性是指当异常发生后,各个对象的状态能够恢复到异常发生前的初始状态。而不至于停留在某个不合理的中间状态。对象的状态是否合理,是由特定问题领域的业务决定的。
保持异常的原子性有以下办法:
先检查方法的参数是否有效,确保当异常发生时还没有改变对象的初始状态。
编写一段恢复代码,由它来解释操作过程中发生的失败,并且使对象状态回滚到初始状态。
在对象的临时副本上进行操作,当操作成功后,把临时副本中的内容复制到原来的对象中。
9.5.5 避免庞大的try代码块
9.5.6 在catch子句中指定具体的异常类型。
9.5.7 不要在catch代码块中忽略被捕获的异常
9.6 记录日志
输出日志的作用:
监视代码中的变量的变化情况,把数据周期性的记录到文件中供其它应用进行统计分析工作。
跟踪代码运行时轨迹,作为日后审计的依据。
承担集成开发环境中的调试器的作用,向文件或控制台打印代码的调试信息。
可以直接使用Java类库中的java.util.logging日志操作包。这个包中主要有4个类:
Logger类:负责生成日志,并能够对日志信息进行分级别筛选,通俗地讲,就是决定什么级别的日志信息应该被输出,什么级别的日志应该被忽略。
Handler类:负责输出日志信息,它有两个子类:ConcoleHandler类(把日志输出到DOS命令行控制台)、和FileHandler类(把日志输出到文件中)
Formatter类:指定日志信息的输出格式。它有两个子类:SimpleFormatter类(常用的日志格式)和XMLFormatter类(表示基于XML的日志格式)
Level类:表示日志的各种级别,它的静态常量表示不同的日志级别。
9.6.1 创建Logger对象及设置日志级别
首先通过Logger类的getLogger(String name)方法获得一个Logger对象
Logger myLogger = Logger.getLogger("myLogger");
在默认情况下,Logger类只输出SEVERE,WARNING, INFO这前三个级别。可以通过Logger类的setLevel()方法设置日志级别。
logger.setLevel("Level.FINE"); // 把日志级别设为FINE
logger.setLevel("Level.WARNING"); // 把日志级别设为WARNING
logger.setLevel("Level.ALL") // 开启所有日志级别
logger.setLevel("Level.OFF") // 关闭所有日志级别
9.6.2 生成日志
Logger类的severe(), warn(), info()方法等分别生成各级级别的日志。
9.6.3 把日志输出到文件
默认情况下,Logger类把日志输出到DOS控制台
9.6.4 设置日志的输出格式
9.7 使用断言
格式:
assert 条件表达式
assert 条件表达式:包含错误信息的表达式
当表达式的值为false时,就会抛出一个AssertError,第二种形式中,第二个表达式会被转换成错误消息的字符串。
当程序运行时,断言在默认情况下是关闭的。