2.2 Exception处理
2.2.1 异常处理机制
传统的异常处理方式一般采用返回值或者信号来标识程序出现的异常情况,这种方式有多个坏处:
- 首先,返回值本身并不能解释是否发生了异常以及该异常的具体情况,需要调用API的程序自己判断并解释返回值的含义
- 其次,无法保证异常情况一定会得到处理,调用程序可以简单的忽略该返回值。
针对上面提到的问题,Java的异常处理机制提供了一种更好的解决方案。通过抛出异常,能够表明程序出现了什么样的异常情况;而且Java的语言机制保证了异常可以会得到恰当的处理。合理的使用异常处理机制,会让程序代码清晰易懂。
2.2.2 Java异常类型
Throwable是所有异常的基类,程序中一般不会直接抛出Throwable对象,Exception和Error是Throwable的子类,Exception又可以分为RuntimeException和CheckedException。可以把Java异常分为三类:
- 第一类是Error:Error表示程序在运行期间出现了十分严重、不可恢复的错误,在这种情况下应用程序只能中止运行,例如JVM出现错误。一般应用程序中既不应该捕获Error,也不应该抛出Error
- 第二类是RuntimeException:编译器不会检查程序是否对RuntimeException作了处理,因此在程序中不用捕获RuntimeException类型的异常,也不必在方法体声明抛出RuntimeException类。一般来说,RuntimeException发生的时候,表示程序中出现了编码错误,应该找出错误修改程序,而不是去捕获RuntimeException
- 第三类是CheckedException:所有直接继承自Exception(而非RuntimeException)的异常都是CheckedException。CheckedException一般用于以下的语义环境:无法通过正确的编码就可以防止,而是在程序运行期间经常会发生的异常情况(比较常见的如IOException等),是编码阶段需要考虑好如何处理的异常流程。例如进行网络操作时网络断链、打开文件时文件已经被删除等。该异常发生后,可以通过对异常的恰当处理,恢复程序原来的正常处理流程。如一个网络连接发生异常被中止后,可以尝试重连,重试成功后再进行后续操作
2.2.3 Java异常处理机制
当程序中抛出一个异常后,程序从程序代码中导致异常的代码处跳出,即try块中出现异常后的代码不会再被执行,JVM会寻找匹配的catch块,如果找到,将控制权交到catch块中的代码,然后继续往下执行程序。如果有finally关键字,程序中抛出异常后,无论该异常是否被catch,都会保证执行finally块中的代码。需要注意的是:在try块和catch块采用return关键字退出本次函数调用,也会先执行finally块代码,然后再退出。由于finally块的这个特性,finally块被用于执行资源释放等清理工作。如果程序发生的异常没有被catch(由于Java编译器的限制,只有RuntimeException才会出现这种情况),执行代码的线程将被异常中止,如果是主线程中止,则会导致程序进程的退出。
2.2.4 Java异常处理的原则
合理使用Java异常机制可以使程序健壮而清晰,但不幸的是,Java异常处理机制常常被错误的使用,下面就是一些关于Exception的注意事项:
不要忽略CheckedException
请看下面的代码:
try {
method1(); //method1抛出ExceptionA
} catch(ExceptionA e) {
e.printStackTrace();
}
由于编译器强制要求处理CheckedException,在很多代码中容易出现上面这种情况,catch异常后只打印一下,然后继续执行下面的代码。上面的代码似乎没有什么问题,事实上在catch块中对发生的异常情况并没有作任何处理(打印异常不能是算是处理异常,因为打印并不能改变程序运行逻辑,修复异常)。这样程序虽然能够继续执行,但是由于这里的操作已经发生异常,将会导致以后的操作并不能按照预期的情况发展下去,可能导致两个结果:
- 一是由于这里的异常导致在程序中别的地方抛出一个异常,这种情况会使之后的调试非常困难,因为新的异常抛出的地方并不是程序真正发生问题的地方,也不是发生问题的真正原因
- 另外一个是程序继续运行,并得出一个错误的输出结果,这种问题更加难以捕捉,因为很可能把它当成一个正确的输出
那么应该如何处理呢,这里有四个选择:
- 处理异常,进行修复以让程序继续执行:例如在进行网络操作时,网络连接断开后可以尝试重连
- 在对异常进行分析后发现这里不能处理它,那么重新抛出异常,让调用者处理
- 将异常转换为其他异常再抛出,这时应该注意不要丢失原始异常信息,这种情况一般用于将底层异常封装为应用层异常
- 不要捕获异常,直接在函数定义中使用throws声明将抛出该异常,让调用者去处理该异常
不要一次捕获所有的异常
请看下面的代码:
try {
method1(); //method1抛出ExceptionA
method2(); //method1抛出ExceptionB
} catch(Exception e) {
……
}
很多人喜欢使用一个catch子句捕获了所有异常,看上去完美而且简洁。但这里有两个潜在的缺陷,一是针对try块中抛出的每种Exception,很可能需要不同的处理和恢复措施,而由于这里只有一个catch块,分别处理就不能实现。二是try块中还可能抛出RuntimeException,代码中捕获了所有可能抛出的RuntimeException而没有作任何处理,掩盖了编程的错误,会导致程序难以调试。下面是改正后的正确代码:
try {
method1(); //method1抛出ExceptionA
method2(); //method1抛出ExceptionB
} catch(ExceptionA e) {
……
} catch(ExceptionB e) {
……
}
使用finally块释放资源
资源是程序中使用的数量有限的对象,或者只能独占式访问的对象。例如:文件、线程、数据库连接、网络连接等。某些资源,使用完毕后会自动释放,如线程;而某些资源则需要显示释放,如数据库连接。
Java保证finally中的语句一定会被执行,因此一般我们会在finally块中释放资源。
finally块不能抛出异常
Java异常处理机制保证无论在任何情况下都会执行finally块的代码。在try-catch块中向外抛出异常的时候,JVM先转到finally块执行finally块中的代码,finally块执行完毕后,再将异常抛出。但如果在finally块中抛出异常,try-catch块的异常就不能抛出,外部捕捉到的异常就是finally块中的异常信息,而try-catch块中发生的真正的异常堆栈信息则丢失了。如下所示:
Connection con = null;
try {
con = dataSource.getConnection();
……
} catch(SQLException e) {
……
throw e;//进行一些处理后再将数据库异常抛出给调用者处理
} finally {
try {
con.close();
} catch(SQLException e) {
e.printStackTrace();
……
}
}
运行程序后,调用者得到的信息如下java.lang.NullPointerException...而不是我们期望得到的数据库异常。
异常不能影响对象的状态
异常产生后不能影响对象的状态,这是异常处理中的一条重要规则。在一个函数中发生异常后,必须确保对象处于正确的状态中。如果对象是不可变对象,那么异常发生后对象状态肯定不会改变。如果是可变对象,必须保证异常不会影响对象状态,有三个方法可以达到这个目的:
- 将可能产生异常的代码和改变对象状态的代码分开
- 对不容易分离产生异常代码和改变对象状态代码的方法,定义一个recover方法,在异常产生后调用recover方法对象状态
- 在方法中使用对象的拷贝,这样当异常发生后,被影响的只是拷贝,对象本身不会受到影响
异常转译时带上原始异常信息
请看下面的代码:
public void method2() {
try {
……
method1(); //method1进行了数据库操作
} catch(SQLExceptione) {
……
throw new MyException(“发生了数据库异常:”+e.getMessage);
}
}
public void method3() {
try {
method2();
} catch(MyExceptione) {
e.printStackTrace();
……
}
}
上面method2的代码中,try块捕获method1抛出的数据库异常SQLException后,抛出了新的自定义异常MyException。这段代码看起来并没有什么问题,但看一下控制台的输出:
MyException:发生了数据库异常:对象名称'MyTable'无效
at MyClass.method2(MyClass.java:232)
at MyClass.method3(MyClass.java:255)
...
而原始异常SQLException的信息丢失了,这里只能看到method2里面定义的MyException的堆栈情况,而method1中发生的数据库异常的堆栈则看不到,造成调试上的困难。在JDK1.4中,Throwable类增加了两个构造方法:
public Throwable(Throwable cause)
public Throwable(String message, Throwable cause)
守护线程中需要catch RuntimeException
一般不应该捕获RuntimeException,而守护线程一般用语提供某种持续的服务,其生命周期一般和整个程序的时间一样长。例如服务器的告警定时同步线程,客户端的告警分发线程。由于守护线程需要长时间提供服务,因此需要catch RuntimeException,避免因为某一次偶发的异常而导致线程被终止。
while (true) {
try {
doSomethingRepeted();
} catch(MyExceptionA e) {
//对checkedexception进行恰当的处理
……
} catch(RuntimeException e) {
//打印运行期异常,用于分析并修改代码
e.printStackTrace();
}
}