异常处理机制和体系结构
Java异常是指在程序运行过程中出现错误,从而影响程序流程的正常运行。而异常处理机制可以保证程序出现错误后,控制接下来的程序流程,是选择定位错误信息,还是抛出异常或捕获异常、还是避免程序非正常退出,都取决于我们。
异常体系结构(附网图一张)
通过上面的异常体系结构图可以清晰看出Java把所有异常都当做对象来处理,所有异常的基类是Throwable类(可抛出),两大子类分别是Error和Exception,Error异常表示程序出现了十分严重、不可恢复的错误,例如Java虚拟机出现错误,这种情况仅凭程序自身是无法处理的,在程序中也不会对Error异常进行捕捉和抛出。Exception又分为RuntimeException(未检查异常)和CheckedException(检查异常),两者区别如下:
RuntimeException(运行时异常):
只有在运行过程中出现错误,才会被检查的异常,是unchecked Exception,编译器不会对运行时异常进行检查和处理。但如果出现了运行时异常,则表明编程出现了错误,则应该找到错误并修改,而不是对其捕获。
CheckedException(检查异常,即非运行时异常):
所有非RuntimeException且来自于Exception的异常都是检查异常,该异常会在程序运行之前被编译器检查,对于检查异常,必须对其进行处理,可以选择捕获或抛出。但应该遵循该原则:谁知情谁处理,谁负责谁处理,谁导致谁处理。
异常处理方式
try、catch、finally、throw、throws,关于它们的用法和注意点,会在下面一一介绍,并附带实例。
- try、catch和finally都是不能单独使用的,只能像try-catch 或者 try-finally,或者三者一起使用try-catch-finally。
- try语句块监控代码,出现异常就停止执行下面的代码,然后将异常移交给catch语句块来处理。
- finally语句块中的代码一定会被执行,常用于回收资源 。
- throws:往上级调用者抛出一个异常。
- throw :抛出一个异常,至于该异常被捕获还是继续抛出都与它无关。
//使用if-else来处理异常
public class Demo01 {
public static void main(String[] args) {
test(5, 0);
}
public static void test(int a, int b) {
//处理除数为0的情况
if(b == 0) {
System.out.println("不能为0");//给出错误提示
System.out.println("程序结束");
}else {
System.out.println("a / b = " + a / b);
}
}
}
如果不使用异常处理机制,用if-else来处理异常,首先影响代码阅读,逻辑层次不清楚,容易把核心代码和异常处理代码搞混,还需要频繁进行逻辑判断,影响程序运行效率,当处理异常过多时,就显得过于鸡肋了,这时候就应该使用异常处理机制来解决。
1.try-catch语句块处理异常
public static void main(String[] args) {
try {
test(5, 0);
}catch(Exception e) {//捕获异常
System.out.println("传值错误,除数不能为0");
System.out.println("程序结束");
e.printStackTrace(); //打印异常信息
}
}
public static void test(int a, int b) {
System.out.println("a / b = " + a / b);
}
比起通过if-else来处理异常的方式,通过try语句块把异常处理和核心代码分开,不仅可以提高可读性,还能通过try语句块来提升程度健壮性,提供更多功能。
上面只是用到try-catch,那么finally语句的用法是怎样呢?实例如下:
2.try-catch-finally处理异常
public class Demo01 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
try {
int a = sc.nextInt(); //从键盘输入一个整型
System.out.println(a);
}catch(Exception e){
System.out.println("输入类型不符");
System.out.println("--catch语句执行完成--");
throw e; //把捕获的异常再次抛出
}finally {
System.out.println("--finally语句被执行--");
}
}
}
//输入数字,没有引发异常,结果:
15
15
--finally语句被执行--
//输入字符,引发异常,结果:
abc
输入类型不符
--catch语句执行完成--
--finally语句被执行--
Exception in thread "main" java.util.InputMismatchException
//省略后续异常定位代码...
上面执行结果表明了finally语句块无论是代码正常执行或是捕获到异常,finally语句块中的代码都会执行。但如果我们在try-catch-finally三个语句块中分别使用return,它们的执行顺序又是怎样呢?
3.try-catch-finally使用return的执行顺序
public class Demo02 {
public static void main(String[] args) {
System.out.println(Demo02.test());
}
public static String test() {
Scanner sc = new Scanner(System.in);
try {
int a = sc.nextInt(); //从键盘输入一个整型
System.out.println(a);
return "try-return";
}catch(Exception e){
System.out.println("--catch语句执行--");
// throw e; 抛出异常后,后面的代码不再执行。
return "catch-return";
}finally {
System.out.println("--finally语句执行--");
return "finally-return";
}
}
}
//输入数字,没有引发异常,结果:
20
20
--finally语句执行--
finally-return
//输入字符,引发异常,结果:
abc
--catch语句执行--
--finally语句执行--
finally-return
结论先不急着给出,我们将finally语句块注释掉,再看看try-catch的return语句又是怎样的执行顺序?
//省略部分没必要的代码
public static String test() {
Scanner sc = new Scanner(System.in);
try {
int a = sc.nextInt(); //从键盘输入一个整型
System.out.println(a);
return "try-return";
}catch(Exception e){
System.out.println("--catch语句执行--");
// throw e; 抛出异常后,后面的代码不再执行。
return "catch-return";
}/** 省略finally语句块 */
}
//try语句块正常执行完成,结果:
10
10
try-return
//引发异常,结果:
abc
--catch语句执行--
catch-return
关于return语句在try-catch-finally语句块的执行顺序结论
- 首先,finally语句块一定会执行,若finally存在return语句,则会优先于try和catch中的return语句执行,当执行了return,则try和catch中的return语句不再执行。
- 如果finally语句中没有return,按照正常流程执行,try或catch在执行return语句之前,会先执行finally中的代码,然后再反回去执行return。
- 如果没有finally语句,则try或catch就按照正常程序流程执行。
- 反过来想,当finally语句存在时,若try或catch执行了return,那finally语句就得不到执行,那finally语句就毫无作用,两者引起悖论。
finally遇见如下情况不会被执行
- 在前面的代码中用了System.exit()退出程序。
- finally语句块中发生了异常。
- 程序所在的线程死亡。
- 关闭CPU。
4.try-finally的应用场景
try-finally通常用来保证某些操作一定会被执行,例如关闭数据库资源、关闭锁等操作。
public static void main(String[] args) {
Connection conn = null;
PreparedStatement pst = null;
ResultSet rs = null;
try {
conn = JDBCUtils.getConnection();
pst = conn.prepareStatement("select * from user");
rs = pst.executeQuery();
//省略后续代码
} catch (SQLException e) {
e.printStackTrace();
}finally {
JDBCUtils.close(rs, pst, conn); //保证打开的资源一定会被关闭
}
}
由于代码篇幅过多,所以上面封装了连接和关闭的代码没给出。
ReentrantLock lock = new ReentrantLock();
try {
//需要加锁的代码
}finally {
lock.unlock(); //保证锁一定被释放
}
使用Lock来保证资源安全时,就需要用到try-finally,如果出现错误,导致锁对象没法释放,那么线程就会出现阻塞,严重时甚至会造成死锁。
抛出异常:throw与throws
throw抛出异常:
throw语句用于抛出异常,可以是Java已有异常,也可以是自定义异常,它只负责抛出,怎么处理和它无关。抛出之后,throw之后的代码不再执行。
throws声明异常:
如果方法中有异常,但又无法处理,就可以使用throws来声明该方法中所有无法处理的异常,用于向上级调用者传递异常。如果有多个异常,则依次在方法签名后排列。
public void getLen(int length)
throws CertificateException, LastOwnerException {
//随便瞎搞的两个异常,别太在意
if(length < 5)
throw new LastOwnerException();
if(length > 10)
throw new CertificateException();
}
throw抛出异常,但未被处理,所以throws对未处理的异常进行声明,交给调用者。例子如下:
public void getLen(int length)
throws CertificateException, LastOwnerException {
//随便瞎搞的两个异常,别太在意
if(length < 5)
throw new LastOwnerException();
if(length > 10)
throw new CertificateException();
}
//无法处理,选择继续抛出
void test1() throws CertificateException, LastOwnerException {
getLen(5);
}
//对异常进行处理
void test2() {
try {
test1();
} catch (CertificateException | LastOwnerException e) {
e.printStackTrace();
}
}
}
最终对其进行捕获进行处理,如果继续往上抛,最后就是交给JVM来处理,但这样做非常不可取。对于异常,有能力处理就处理!上面同时捕获多个异常,触发其中一个异常都会进行统一处理,也可以对每一个异常进行分别捕捉处理。这需要根据你的要求来定。
声明异常和捕获异常应遵循的原则:
- 声明异常的第一位不要声明Exception异常,因为Exception是所有检查异常的父类,所以会忽略剩下的异常。
- 捕捉异常时第一位不要捕获Exception,应该放在最后面,保证前面的异常能被捕获到。
- 即父类异常要放在子类异常之后,避免异常匹配时被屏蔽。
自定义异常
要明白一点:异常最有用的信息就是异常名,但有时候对于某些情况,Java自带异常不能很好地进行描述时,就可以使用自定义异常来进行准确描述。
一个简单的自定义异常:
public class LengthException extends Exception {
public LengthException() {}
public LengthException(String s) {
super(s);
}
@Override
public String getMessage() {
return super.getMessage();
}
}
public class Demo02 {
public static void main(String[] args) {
try {
test(3);
} catch (LengthException e) {
e.printStackTrace();
}
}
public static void test(int len) throws LengthException {
if(len < 5) throw new LengthException();
System.out.println(len);
}
//处理异常,结果:
com.exceptions.test.LengthException
}
自定义异常就是一个普通类继承Exception类就可以,对于自定义异常,不需要太多功能,类名能准确描述问题是关键。
《Think in Java》中对异常的总结
- 在恰当的级别处理问题。(在知道该如何处理的情况下了捕获异常。)
- 解决问题并且重新调用产生异常的方法。
- 进行少许修补,然后绕过异常发生的地方继续执行。
- 用别的数据进行计算,以代替方法预计会返回的值。
- 把当前运行环境下能做的事尽量做完,然后把相同的异常重抛到更高层。
- 把当前运行环境下能做的事尽量做完,然后把不同的异常抛到更高层。
- 终止程序。
- 进行简化(如果你的异常模式使问题变得太复杂,那么用起来会非常痛苦)。
- 让类库和程序更安全。