Java 中的 Exception 和 Error
Contents
程序的异常处理在开发中很是常见,只有正确处理好意外情况,才能保证程序的可靠性。本文通过 Java 异常处理机制,详细的分析与解释 Java 中的 Exception 与 Error 之间的区别以及需要注意的点,以便更好的实现程序的可维护性。
如何区分 Exception 和 Error
在 Java 的异常处理机制中,有许多捕获由于产生的不同异常而制定不同的异常类,各异常类之间的关系如下图所示:

从上图可以看出,Exception和Error都继承了Throwable类。一般我们在编写代码的时候,假如语法使用错误,编辑器通常会给与提醒,我们是很容易发现并改正的。但在编译期间发生的错误是不能全部找到的,也就是说,某一些错误只有在程序运行期间才能显现出来,这类错误就属于Throwable,它可以被抛出或者被捕获,同时也是异常处理机制的基本组成类型。
对于异常的抛出,首先会在堆上创建异常对象,然后当前执行的路径会被终止,并且从当前环境中弹出对异常对象的引用,最后的程序由异常处理机制进行接管,从而将程序从错误状态中恢复。
下面对Exception和Error进行展开说明:
Error是指在正常情况下,不大可能出现的情况,它属于程序本身无法处理的错误,通常与 JVM、环境资源有关。因为假如发生了此类情况,它将会导致程序处于非正常的、不可恢复的状态,一般会终止线程。这些错误是不能事先知道的,或者说是不可查的,并且处于应用程序的控制和处理能力之外。常见的比如 OutOfMemoryError 类。
Exception属于程序本身可以进行处理的错误,可以在程序运行时将故障进行抛出。从图中可以看到,它含有:
运行时异常:例如常见的 NullPointerException、IndexOutOfBoundsException 、ClassCastException 等;其它异常(也称非运行时异常):例如常见的 IOException、SQLException 等。
将以上的异常进行分类的话,通常分为:
-
非检查异常(unchecked exception):包括 RuntimeException 及其子类和 Error,此类异常在编译时不要求必须进行异常捕获或抛出,即假如程序中可能出现此类异常,即使没有进行捕获或者抛出,也会编译通过。 -
检查异常(checked exception):此类异常必须进行处理,应进行捕获(try-catch)或者抛出(throws),只有这样编译器才会通过。
如何处理异常
一般情况下可以使用try-catch语句进行异常的处理,如下所示:
|
|
当try中发生异常的时候,异常处理机制会负责寻找与当前异常相匹配的异常类型,然后进入对应的catch中执行相应的异常处理语句,一旦异常得到处理(catch语句执行结束),则异常处理程序就结束了。也就是说,如果没有发生异常,则catch语句块不会执行,如果发生了异常,则按照catch语句块的先后顺序进行匹配,只要匹配成功,就执行catch中的处理逻辑,那么后面的catch就不会执行了。
其中catch子句必须与try子句同时使用,假如引入了finally子句,则会存在以下三种格式:
当然,
try语句块只能有一个,catch语句块可以有多个,finally语句块最多只能有一个,也可以没有。
|
|
|
|
|
|
但需要注意一点的是:finally子句总是会执行的,但前提条件是try子句执行。如下所示:
|
|
输出结果如下:
|
|
由于没有执行try语句,所以finally子句也没有执行。
为了弄清楚这些语句与return语句(控制转移语句)之间的关系,下面给出一些例子以进行区分,相关引用已在参考中给出链接:
|
|
输出结果如下:
|
|
从执行结果可看出,假如try中没有发生异常的语句,则finally语句会在try中的return语句执行之前执行。再看下面代码:
|
|
输出结果如下:
|
|
从结果可以看出,在进入try块后,首先打印了try block,由于发生了异常,会被catch捕获,继而输出exception block,最后输出finally block语句。
值得注意的是,test()方法最终返回的是2,这就说明finally语句是在catch中的return执行之前执行的。同时,当i = 1 / 0;语句发生异常后,紧跟着后面的return 1;语句也就不会执行了。此时,如果将return 1;或return 2;任意一个注释掉的话,则会提示 缺少返回语句。再看如下代码:
|
|
输出结果如下:
|
|
从结果可以看出,在finally块中使用了return;语句的前提下,即使try捕获到了异常,也会执行finlly块。
综上所述,finally块是在try或者catch中的return语句之前执行的,类似于return的控制转移语句还有break、continue和throw。再看最后一段代码:
|
|
输出结果如下:
|
|
咦?不是说finally块是在try或者catch中的return语句之前执行的的吗?return statement语句怎么会在finally block之前就打印了呢?
其实,第10行中的return test1();相当于下面这两条语句:
|
|
也就是说,当执行到return test1();语句的时候:
- 首先会执行
String temp = test1();语句,则会先输出return statement; - 然后再将返回值
after return交给temp,其次再执行return temp;语句; - 但是在执行
return temp;语句之前,会先执行finally中的finally block,所以最后输出的就是after return了。
经过上面的例子,你可能已经掌握了try-catch-finally的执行顺序,但且慢,来看最后两个小例子:
|
|
输出结果如下:
|
|
还有一个例子:
|
|
输出结果如下:
|
|
按照之前的逻辑,最终的结果应该是2才对,为什么结果是1呢?
关于 Java 虚拟机是如何编译finally语句块的问题,有兴趣的可以参考《The JavaTM Virtual Machine Specification, Second Edition》中 3.13 节的 Compiling finally,那里详细介绍了 Java 虚拟机是如何编译 finally 语句块。
实际上,Java 虚拟机会把finally语句块作为 subroutine 直接插入到try语句块或者catch语句块的控制转移语句之前。但是,还有另外一个不可忽视的因素,那就是在执行 subroutine(也就是finally语句块)之前,try或者catch语句块会保留其返回值到 **本地变量表(Local Variable Table)**中。待 subroutine 执行完毕之后,再恢复保留的返回值到操作数栈中,然后通过return或者throw语句将其返回给该方法的调用者(invoker)。请注意,对于这条规则(保留返回值),只适用于return和throw语句,不适用于break和continue语句,因为它们根本就没有返回值。