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
语句,因为它们根本就没有返回值。