基本定义
异常的处理机制可以确保程序的健壮性,提高系统可用率。
在OO中提供的异常处理机制是提供代码健壮的强有力的方式。使用异常机制它能够降低错误处理代码的复杂度,如果不使用异常,那么就必须检查特定的错误,并在程序中的许多地方去处理它,而如果使用异常,那就不必在方法调用处进行检查,因为异常机制将保证能够捕获这个错误,并且,只需在一个地方处理错误,即所谓的异常处理程序中。这种方式不仅节约代码,而且把“概述在正常执行过程中做什么事”的代码和“出问题怎么办”的代码相分离。总之,与以前的错误处理方法相比,异常机制使代码的阅读、编写和调试工作更加井井有条。(摘自《Think in java 》)。
在《Think in java》中是这样定义异常的:异常情形是指阻止当前方法或者作用域继续执行的问题。在这里一定要明确一点,异常代码某种程度的错误,尽管Java有异常处理机制,但是我们不能以“正常”的眼光来看待异常,异常处理机制的原因就是告诉你:这里可能会或者已经产生错误,您的程序出现不正常的情况,可能会导致程序失败!
那么什么时候才会出现异常呢?只有在你当前的环境下程序无法正常运行下去,也就是说程序已经无法来正确解决问题,这时它所就会从当前环境中跳出,并抛出异常。
抛出异常后,它首先会做几件事。首先,它会使用new创建一个异常对象,然后在产生异常的位置终止程序,并且从当前环境中弹出对异常对象的引用。这时,异常处理机制就会接管程序,并开始寻找一个恰当的地方来继续执行程序,这个恰当的地方就是异常处理程序,它的任务就是将程序从错误状态恢复,以使程序要么换一种方法执行,要么继续执行下去。
总的来说异常处理机制就是当程序发生异常时,它强制终止程序运行,记录异常信息并将这些信息反馈给我们,由我们来确定是否处理异常。
异常体系
Java提供非常完美的异常处理机制,从下面这幅图可以看出,Throwable是Java语言中所有错误和异常的超类。它有两个子类:Error、Exception。
Error
Error为错误,是程序无法处理的(表示仅靠程序本身无法恢复的严重错误),如OutOfMemoryError、ThreadDeath等,交由JVM来处理,不过JVM在大多数情况下会选择终止线程。(程序本身无法修复这些错误。一般不去扩展Error类来创建用户自定义的错误类。也不用去try..catch
这类异常)
Error是Unchecked Exception,编译器不会检查Error是否被处理,在程序中不用捕获Error类型的异常。
Exception
Exception是程序可以处理的异常。它又分为两种Checked Exception(受检查异常),一种是Unchecked Exception(不受检异常)。
- Check Exception:发生在编译阶段,必须要使用
try…catch
(或者throws
)否则编译不通过。发现错误的最佳时期是编译期间(检测性异常),然而编译期间并不能找出所有数据,余下的问题必须在运行期间解决。 - Unchecked Exception:发生在运行期,具有不确定性,主要是由于程序的逻辑问题所引起的,难以排查。发生异常,尽量处理异常,即使产生异常,也能尽量保证程序朝着有利方向发展。
所以,对于可恢复的条件使用被检查的异常(Checked Exception)进行try...catch
捕获,对于程序错误(言外之意不可恢复,大错已经酿成,程序自己的错误)使用运行时异常(RuntimeException)不使用try..catch
捕获,由JVM处理,一般终止进程。
另一方面,我们直接将异常分为非运行时异常(Check Exception)和运行时异常(Unchecked Exception)。
异常的捕获就要做一些有意义的处理,比如受检查的异常,在catch
中就可以恢复程序。
Runtime Exception
RuntimeException是那些可能在Java虚拟机正常运行期间抛出的异常的超类(空指针,越界)。Java编译器不去检查它,也就是说,当程序中可能出现这类异常时,即使没有用try...catch
语句捕获它,也没有用throws
字句声明抛出它,还是会编译通过,这种异常可以通过改进代码实现来避免。(这类异常程序中可以选择捕获处理,也可以不处理,这类异常一般是由于代码逻辑错误。尽量去避免这种异常,一旦发现该异常,正确的做法就会改进程序设计的代码和实现方式,修改程序中的错误。)
RuntimeException发生的时候,表示程序中出现编程错误,所以应该找出错误修改程序,而不是去捕获RuntimeException。
因此在做异常体系设计时要根据错误的性质合理选择自定义异常的继承关系。
异常使用
系统异常
在异常中try快包含着可能出现异常的代码块,catch块捕获异常后对异常进行处理。
实例1
1 | public class ExceptionTest { |
1 | java.io.FileNotFoundException: D:\exceptionTest.txt (系统找不到指定的文件。) |
从这个结果可以看出这些
- 当程序遇到异常时会终止程序的运行(即后面的代码不在执行),控制权交由异常处理机制处理。
catch
捕捉异常后,执行里面的函数。实例2
1
2
3
4
5
6
7public class ExceptionTest {
public staticvoid main(String[] args) {
int[] a = {1,2,3,4};
System.out.println(a[4]);
System.out.println("我执行了吗???");
}
}
运行程序结果如下1
2Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 4
at com.test9.ExceptionTest.main(ExceptionTest.java:14)
实例1和实例2的异常放在一起看1
2java.io.FileNotFoundException: D:\exceptionTest.txt (系统找不到指定的文件。) //实例1
Exception in thread "main"java.lang.ArrayIndexOutOfBoundsException: 4 //实例2
区别:第二个异常信息多一条Exception in thread "main"
(这里异常是由JVM进行处理),这显示出现异常信息的位置。
在这里可以得到如下结论,若程序中显示的声明某个异常,则抛出异常时不会显示出处,若程序中没有显示的声明某个异常,当抛出异常时,系统会显示异常的出处,但是最终会有异常栈信息打印出来at com.test9.ExceptionTest.main(ExceptionTest.java:19)
。
实例3
1 | package demo1; |
1 | 进入g的异常........... |
结论:A()
体内调用另一个T()
,这个T()`
throw抛出异常或者不处理,
A()捕获
T(),
A()中捕获
T()中的异常,那么
A()中的
catch就会处理异常,但是
T()`异常出现位置的后续程序都不会执行。(有异常,程序中断,后续程序不会执行)
自定义异常
Java自定义异常的使用要经历如下四个步骤:
- 定义一个类继承Throwable或其子类(RuntimeException,即Unchecked Exception)。
- 添加构造方法(当然也可以不用添加,使用默认构造方法)。
- 在某个方法类抛出该异常。
- 捕捉该异常。
异常处理步骤
- 调用异常的对象的
printStackTrace()
,打印方法调用栈的异常信息。 - 如果出现异常的线程为主线程,则整个程序运行终止;如果非主线程,则终止该线程,其他线程继续运行。
finally
语句在任何情况下都必须执行的代码,这样可以保证一些在任何情况下都必须执行代码的可靠性。比如,在数据库查询异常的时候,应该释放JDBC连接等等。
通过分析思考可以看出,越早处理异常消耗的资源和时间越小,产生影响的范围也越小。因此,不要把自己能处理的异常也抛给调用者。finally
语句先于return语句执行,而不论其先后位置,也不管是否try
块出现异常。finally
语句唯一不被执行的情况是方法执行System.exit(0)
。System.exit(0)
的作用是终止当前正在运行的Java虚拟机。(终止线程)finally
语句块中不能通过给变量赋新值来改变return的返回值,也建议不要在finally
块中使用return语句,没有意义还容易导致错误。(finally
中有return,程序会返回此return,因为finally
优先于try
的return执行,so,程序是返回finally
的return!实际上就是finally
的return会覆盖try
的return)
异常链
在设计模式中有一个叫做责任链模式,该模式是将多个对象链接成一条链,客户端的请求沿着这条链传递直到被接收、处理。同样Java异常机制也提供这样一条链:异常链。
有两种方式处理异常,一是throws抛出交给上级处理,二是try…catch
做具体处理。try…catch
的catch
块可以不需要做任何处理,仅仅只用throw这个关键字将封装异常信息主动抛出来。然后在通过关键字throws
继续抛出该方法异常。 它的上层也可以做这样的处理,以此类推就会产生一条由异常构成的异常链。
通过使用异常链,我们可以提高代码的可理解性、系统的可维护性和友好性。
同理,有时候在捕获一个异常后抛出另一个异常信息(异常转移),并且希望将原始的异常信息也保持起来,这个时候也需要使用异常链。
在异常链的使用中,throw
抛出的是一个新的异常信息,这样势必会导致原有的异常信息丢失,如何保持?在Throwable
及其子类中的构造器中都可以接受一个cause参数,该参数保存原有的异常信息,通过getCause()
就可以获取该原始异常信息。
实例
1 | public class Test { |
运行结果:优先打印出try..catch
的异常或者由JVM处理的异常信息,栈是先进后出。这里先进入是main()
,然后g()
,最后f()
。1
2
3
4
5
6
7
8
9
10
11
12
13com.test9.MyException: 文件没有找到--02
at com.test9.Test.g(Test.java:31)
at com.test9.Test.main(Test.java:38)
Caused by: com.test9.MyException: 文件没有找到--01 这里才是异常链
at com.test9.Test.f(Test.java:22)
at com.test9.Test.g(Test.java:28)
... 1 more
Caused by: java.io.FileNotFoundException: G:\myfile\struts.txt (系统找不到指定的路径。)JVM处理的异常信息 at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.<init>(FileInputStream.java:106)
at java.io.FileInputStream.<init>(FileInputStream.java:66)
at java.io.FileReader.<init>(FileReader.java:41)
at com.test9.Test.f(Test.java:17)
... 2 more
异常的使用误区
1 | OutputStreamWriter out = null; |
减小try代码块
对于这个try…catch
块是为捕获SQL的异常,但是这个try
块是不是包含太多的信息。这是代码坏习惯。将一大块的代码全部包含在一个try
块里面,因为这样省事,反正有异常它就会抛出,而不愿意花时间来分析这个大代码块有哪些会产生异常,产生什么类型的异常,反正就是一篓子全部搞定。所有对于一个异常块,应该仔细分清楚每块的抛出异常,因为一个大代码块有太多的地方会出现异常。
解决方式1:把一个catch()拆分成N个catch(),分别进行异常捕获1
2
3
4
5
6
7
8
9try{
}catch(IOException e1){
}catch(SQLException e2){
}catch(Exception e){
}
解决方式2:使用N个try…catch()进行捕获异常1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17try{
}catch(IOException e1){ //IO Exception
}
try{
}catch(SQLException e2){ //SQL Exception
}
try{
}catch(Exception e){
}
保证所有资源都被正确释放,充分运用finally关键词
如果该程序发生异常那么conn.close()
; out.close()
; 是不可能执行得到的,这样势必会导致资源不能释放掉。所以如果程序用到文件、Socket、JDBC连接之类的资源,即使遇到异常,也要确保能够正确释放占用的资源。这里finally
就有用武之地:不管是否出现异常,finally
总是有机会运行的,所以finally
用于释放资源是再适合不过。
catch语句应当尽量指定具体的异常类型,而不应该指定涵盖范围太广的Exception类
首先catch块所表示是它预期会出现何种异常,并且需要做何种处理,而使用Exception就表示要处理所有的异常信息,但是不可能处理所有异常。不要一个Exception
试图处理所有可能出现的异常。主要是特定异常,有特定的修复方法。
补充:用Exception
要放在最后一个catch
,保证所有意料之外的异常都能被捕获,但是catch(Exception e)
虽然能捕获所有异常对象,但是其处理方法却只能有一种,无针对性,不建议这么使用。
这里再来看看上面的程序实例,很显然它可能需要抛出两个异常信息,SQLException和IOException。所以一个catch
处理两个截然不同的Exception
明显的不合适(if...else
可以处理)。如果用两个catch
,一个处理SQLException
、一个处理IOException
就好多。
既然捕获异常,就要对它进行适当的处理
这里涉及到两个问题,一是,捕获异常不做处理,二是异常信息不够明确。
尽量不要在catch
中就用一句话来处理异常ex.printStackTrace()
。有异常时要处理的。
异常处理方法
- 处理异常。对所发生的的异常进行一番处理,如修正错误、提醒。再次申明
ex.printStackTrace()
算不上已经“处理好异常”。 - 重新抛出异常。如果本方法没有办法处理异常,则抛给上一个方法。
- 封装异常。对异常信息进行分类,然后进行封装处理。
- 不要捕获异常。则发生异常抛给JVM处理。
- 异常信息不明确。对于这样的
java.io.FileNotFoundException: ………!
所以在出现异常后,最好能够提供一些文字信息,例如当前正在执行的类、方法和其他状态信息,包括以一种更适合阅读的方式整理和组织printStackTrace
提供的信息。对代码层次结构的污染
经常将代码分 Service、Business Logic、DAO 等不同的层次结构,DAO 层中会包含抛出异常的方法,如所示1
2
3public Customer retrieveCustomerById(Long id) throw SQLException {
//根据 ID 查询数据库
}
上面这段代码没什么问题,但是从设计耦合角度仔细考虑一下,这里的SQLException
污染到上层调用代码,调用层需要显式的利用try-catch
捕捉,或者向更上层次进一步抛出。根据设计隔离原则,可以适当修改成:使用非检测性的异常封装检测性异常,向上一层进行抛Exception
。1
2
3
4
5
6
7
8
9
10
11public Customer retrieveCustomerById(Long id) {
try{
//根据ID查询数据库
}catch(SQLException e){
//利用非检测异常封装检测异常,降低层次耦合
//对异常进行转换,抛向上一层
throw new RuntimeException(SQLErrorCode, e);
}finally{
//关闭连接,清理资源
}
}
将异常包含在循环语句块中
异常在for
里和for
外,整体性能差异不大,根据业务来实现不同try...catch...
方式。
如果需要在
for
执行时候报异常,下一次for
依然要执行。try...catch
放在for
中。因为try...catch...
只捕获当前的异常,然后执行catch
之后的程序。1
2
3
4
5
6
7for(,,){
try{
}catch(Exception e){
}
}在for执行时候报异常,不执行下一次for语句,直接执行for代码块后面的程序。
try...catch...
放在for外。1
2
3
4
5
6
7try{
for(,,){
}
}catch(Exception e){
}
总结
Think in java
应该在下列情况下使用异常。
- 在恰当的级别处理问题,在知道该如何处理异常的情况下才捕获异常,未有异常的代码,可以不做捕获。
- 解决问题并且重新调用产生异常的方法。
- 进行少许修补,然后绕过异常发生的地方继续执行。能修补就修补,不能继续执行异常后程序。如果一段业务需要执行,但是这段业务不影响主要业务,报错,需要继续执行后续业务,则这段业务捕获异常,出错就会执行异常后的代码。
- 用别的数据进行计算,以代替方法预计会返回的值。比如,返回一个默认值。
- 把当前运行环境下能做的事情尽量做完。然后把相同的异常重新抛到更高层。比如,Dao->Service->Controller
- 终止程序。
- 进行简化。
- 让类库和程序更加安全。这既是在为调试做短期投资,也是在为程序的健壮做长期投资。
- 异常的重要准则是只有在知道如何处理的情况下才捕获异常。把错误处理的代码同错误发生的地点相分离。举个列子service-dao两层,在dao里产生SQLException我无需处理直接抛出,到service层,我知道要将其转换成自己的业务异常。异常的转移,异常链,在Action转换成客户端的错误!
Effective java
- 基于异常的模式:用
try...catch
,不能忽略异常打印的信息,以免照成有异常抛出,也不能及时发现(catch
放空) - 异常应该只用于异常的情况下,它们永远不应该用于正常的控制流。
- 对可恢复的情况使用受检异常,对编程错误使用运行时异常。何为可恢复的情况,是指不改变代码的基础上,这个异常情况是可以恢复的,比喻
IOExcepion
可能是网络的问题,解决网络,代码就能继续运行下去;何为错误,当然指不能恢复的啦,比喻从你控制范围之外传递进来的null引用,所以,此类异常都是程序员的问题,作为程序员,应该在代码中进行必要的检查。 - 异常类越少,意味着内存印迹就越小,装载这些类的时间开销也越少。
- 异常转译:高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。异常链就是一种特殊的异常转译形式,高层异常可以通过
getCause
来获得低层的异常。 - 异常的细节信息应该包含所有的对该异常有贡献的参数和域的值。比喻
IndexOutOfBoundsException
应该包含lowerBound
,upperBound
,Index
三个参数。遗憾的是Java平台里并没有广泛的使用这种做法,而是全部清一色的简单继承下父Exception
。 - 失败原子性:失败的方法调用应该使对象保持在调用前的状态。原子性的操作,应该保证之前对象的状态。