吹逼两小时,代码五分钟。平常写代码中异常无处不在,早就练就了我们try{...} actch(){...}finally{...}
一把梭的深厚功力。毕竟它看起来真的很简单,我们用着也蛮顺心,但是你真的了解它吗,真的不会出错吗?
Java异常体系
下面是Java中的异常继承图:
Throwable
是整个异常体系中的顶级父类,它拥有两个子类,分别是Error
和Exception
。
Error
是由机器底层抛出的错误,我们无法处理的,比如OutOfMemoryError
,当遇到这类错误时,JVM
会直接终止进程,应用终止。此类异常我们不要去捕获,因为捕获了也处理不了。
Exception
是程序可以处理的错误,主要分为运行时异常,和非运行时异常。或者叫做受检异常和非受检异常。
运行时异常都是RuntimeException
及其子类,比如,NullPointException
、ArrayIndexOutOfBoundsException
等等,这类的异常属于非受检异常(UnChecked
),我们可以对其捕获处理,也可以不处理,而我们一般也不会做处理的,因为这类错误通常是我们逻辑错误所导致的,我们应该尽量避免此类Bug
。
而非运行时异常,比如IOException
、EOFException
等等所有非RuntimeException
及其子类,都是非运行时异常,也就是受检异常(Checked
),当方法抛出了这类异常,则调用者必须在调用该方法时对其进行处理,否则将无法编译。
异常处理
那么我们该怎么去处理异常呢,这时候自然而然会想到try-catch-finally
,我们来下面的代码:
public static void main(String[] args) {
System.out.println(test1());
}
public static boolean test1() {
boolean flag = true;
try {
flag = test2();
} catch (Exception e) {
System.out.println("[test1] catch exception result=" + flag);
flag = false;
throw e;
} finally {
System.out.println("[test1] finally result=" + flag);
return flag;
}
}
public static boolean test2() throws Exception {
boolean flag = true;
try {
// 分别调用test3()和test4()
flag = test4();
if (!flag) {
return false;
}
System.out.println("[test2] result=" + flag);
return flag;
} catch (Exception e) {
System.out.println("[test2] catch exception result=" + flag);
flag = false;
throw e;
} finally {
System.out.println("[test2] finally result=" + flag);
return flag;
}
}
public static boolean test3() throws Exception {
boolean flag = true;
try {
System.out.println("[test3] result=" + flag);
return true;
} catch (Exception e) {
System.out.println("[test3] catch exception result=" + flag);
flag = false;
throw e;
} finally {
System.out.println("[test3] finally result=" + flag);
return flag;
}
}
public static boolean test4() throws Exception {
boolean flag = true;
try {
int a = 2 / 0;
System.out.println("[test4] result=" + flag);
return true;
} catch (Exception e) {
System.out.println("[test4] catch exception result=" + flag);
flag = false;
throw e;
} finally {
System.out.println("[test4] finally result=" + flag);
return flag;
}
}
当其中test2()
方法分别调用test3()
和teset4()
,打印顺序会是怎么样的呢,不如先思考一下。
调用test3()
时,打印结果:
[test3] result=true
[test3] finally result=true
[test2] result=true
[test2] finally result=true
[test1] finally result=true
true
调用test4()
时,打印结果:
[test4] catch exception result=true
[test4] finally result=false
[test2] finally result=false
[test1] finally result=false
false
不知道这段代码的实际执行结果和你所想的有没有出入,众所周知的是,当try{···}
块中的代码执行未发生异常,贼执行finally{...}
块中的代码,如果执行出现异常,则会先执行catch(){...}
中的代码,再执行finally{...}
中的代码。但是当这些代码块中出现return
、throw
则会发生变化。
当try{···}块或者catch(){…}块中有return、throw时,在return或者throw执行前回优先执行finally{…}代码块中的内容
特别提醒,
finally
中禁止使用return
,这里使用,只是为了演示用。
所以当finally
中有throw
或者return
时,它会覆盖try
块中和catch
块中的throw
以及return
,从而出现异常屏蔽现象,比如下面这段代码:
public static void main(String[] args) {
try {
int i = 1 / 0;
} catch (Exception e) {
throw new RuntimeException("catch");
} finally {
throw new RuntimeException("finally");
}
}
它将打印出:
Exception in thread "main" java.lang.RuntimeException: finally
at top.felixu.exception.ExceptionDemo1.main(ExceptionDemo1.java:14)
catch
块中的异常就被覆盖了,这种现象还是有可能出现在try-catch-finally
捕获处理流相关操作中,在finally
中手动关闭流出现异常时,但是在JDK7
版本以后,try-with-resource
可以帮我们自动关闭流了。在JDK7
中,所有的IO
类都实现了AutoCloseable
接口,并且需要实现其中的close()
函数,资源释放过程需要在该函数中完成。那么,编译器在编译时,会自动添加finally
代码块,并将close()
函数中的资源释放代码加入finally
代码块中,从而提高代码可读性。这里就不具体介绍了。
异常处理约定
- 对于非受检异常,我们不要去捕获处理,而是通过测试和
review
代码来规避此类问题。 - 异常不要用来做流程控制,条件控制,因为异常处理的效率比分支处理要低。
- 对大块代码的
try-catch
是不负责的行为,我们要区分稳定代码已经不稳定代码。 - 捕获异常是为了处理异常,如果捕获了什么都不处理,不如不捕获,将其抛给其上层调用者。而最上层的调用者必须处理,防止用户看到无法理解的异常信息。
try
块中有事物代码,则catch
到异常要手动回滚,或者抛出异常,让AOP
框架来回滚,否则会吞掉异常,导致异常却未回滚。- 不能在
finally
中使用return
。 - 捕获异常必须与所抛出的异常匹配,或者所捕获的异常是抛出异常的父类。