1 异常的继承体系结构
- Throwable 类是 Java 语言中所有错误或异常的超类。
- 只有当对象是此类(或其子类之一)的实例时,才能通过 Java 虚拟机或者 Java throw 语句抛出。类似地,只有此类或其子类之一才可以是 catch 子句中的参数类型。
- Throwable 包含了其线程创建时线程执行堆栈的快照。它还包含了给出有关错误更多信息的消息字符串。
- 最后,它还可以包含 cause(原因):另一个导致此 throwable 抛出的 throwable。此 cause 设施在 1.4 版本中首次出现。它也称为异常链 设施,因为 cause 自身也会有 cause,依此类推,就形成了异常链,每个异常都是由另一个异常引起的。
1.1 Error
- Error 是 Throwable 的子类,用于指示合理的应用程序不应该试图捕获的严重问题。
- 大多数这样的错误都是异常条件。虽然 ThreadDeath 错误是一个“正规”的条件,但它也是 Error 的子类,因为大多数应用程序都不应该试图捕获它。
- 在执行该方法期间,无需在其 throws 子句中声明可能抛出但是未能捕获的 Error 的任何子类,因为这些错误可能是再也不会发生的异常条件。
- Java 程序通常不捕获错误。错误一般发生在严重故障时,它们在Java程序处理的范畴之外。
1.2 Exception
-
Exception 异常主要分为两类
- 一类是 IOException(I/O 输入输出异常),其中 IOException 及其子类异常又被称作「受查异常」
- 另一类是 RuntimeException(运行时异常),RuntimeException 被称作「非受查异常」。
受查异常就是指,编译器在编译期间要求必须得到处理的那些异常,你必须在编译期处理了。
1.2.1 常见的非检查性异常:
1.2.2 常见的检查性异常:
2 自定义异常类型
Java 的异常机制中所定义的所有异常不可能预见所有可能出现的错误,某些特定的情境下,则需要我们自定义异常类型来向上报告某些错误信息。
- 在 Java 中你可以自定义异常。编写自己的异常类时需要记住下面的几点。
- 所有异常都必须是 Throwable 的子类。
- 如果希望写一个检查性异常类,则需要继承 Exception 类。
- 如果你想写一个运行时异常类,那么需要继承 RuntimeException 类。
3 异常的处理方式
3.1 try...catch关键字
- 使用 try 和 catch 关键字可以捕获异常。
- try/catch 代码块放在异常可能发生的地方。
try/catch代码块中的代码称为保护代码,使用 try/catch 的语法如下:
try {
// 程序代码
} catch(ExceptionName e1) {
//Catch 块
}
- Catch 语句包含要捕获异常类型的声明。当保护代码块中发生一个异常时,try 后面的 catch 块就会被检查。如果发生的异常包含在 catch 块中,异常会被传递到该 catch 块,这和传递一个参数到方法是一样。
- 一个 try 代码块后面跟随多个 catch 代码块的情况就叫多重捕获。
- 多重捕获块的语法如下所示:
try{
// 程序代码
}catch(异常类型1 异常的变量名1){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}
3.2 throws/throw 关键字
- 如果一个方法没有捕获一个检查性异常,那么该方法必须使用 throws 关键字来声明。throws 关键字放在方法签名的尾部。也可以使用 throw 关键字抛出一个异常,无论它是新实例化的还是刚捕获到的。
- 下面方法的声明抛出一个 RemoteException 异常:
public class className {
public void deposit(double amount) throws RemoteException {
// Method implementation
throw new RemoteException();
}
//Remainder of class definition
}
一个方法可以声明抛出多个异常,多个异常之间用逗号隔开。
3.3 finally关键字
- finally 关键字用来创建在 try 代码块后面执行的代码块。
- 无论是否发生异常,finally 代码块中的代码总会被执行。在 finally 代码块中,可以运行清理类型等收尾善后性质的语句。
- finally 代码块出现在 catch 代码块最后,语法如下:
try{
// 程序代码
}catch(异常类型1 异常的变量名1){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}finally{
// 程序代码
}
4 try-catch-finally 的执行顺序
try-catch-finally 执行顺序的相关问题可以说是各种面试中的「常客」了,尤其是 finally 块中带有 return 语句的情况。我们直接看几道面试题:
4.1 面试题一:
public static void main(String[] args){
int result = test1();
System.out.println(result);
}
public static int test1(){
int i = 1;
try{
i++;
System.out.println("try block, i = "+i);
}catch(Exception e){
i--;
System.out.println("catch block i = "+i);
}finally{
i = 10;
System.out.println("finally block i = "+i);
}
return i;
}
输出结果如下:
try block, i = 2
finally block i = 10
10
这算一个相当简单的问题了,没有坑,下面我们稍微改动一下:
public static int test2(){
int i = 1;
try{
i++;
throw new Exception();
}catch(Exception e){
i--;
System.out.println("catch block i = "+i);
}finally{
i = 10;
System.out.println("finally block i = "+i);
}
return i;
}
输出结果如下:
catch block i = 1
finally block i = 10
10
4.2 面试题二
public static void main(String[] args){
int result = test3();
System.out.println(result);
}
public static int test3(){
//try 语句块中有 return 语句时的整体执行顺序
int i = 1;
try{
i++;
System.out.println("try block, i = "+i);
return i;
}catch(Exception e){
i ++;
System.out.println("catch block i = "+i);
return i;
}finally{
i = 10;
System.out.println("finally block i = "+i);
}
}
输出结果如下:
try block, i = 2
finally block i = 10
2
是不是有点疑惑?明明我 try 语句块中有 return 语句,可为什么最终还是执行了 finally 块中的代码?
我们反编译这个类,看看这个 test3 方法编译后的字节码的实现:
0: iconst_1 //将 1 加载进操作数栈
1: istore_0 //将操作数栈 0 位置的元素存进局部变量表
2: iinc 0, 1 //将局部变量表 0 位置的元素直接加一(i=2)
5: getstatic #3 // 5-27 行执行的 println 方法
8: new #5
11: dup
12: invokespecial #6
15: ldc #7
17: invokevirtual #8
20: iload_0
21: invokevirtual #9 24: invokevirtual #10
27: invokevirtual #11
30: iload_0 //将局部变量表 0 位置的元素加载进操作栈(2)
31: istore_1 //把操作栈顶的元素存入局部变量表位置 1 处
32: bipush 10 //加载一个常量到操作栈(10)
34: istore_0 //将 10 存入局部变量表 0 处
35: getstatic #3 //35-57 行执行 finally中的println方法
38: new #5
41: dup
42: invokespecial #6
45: ldc #12
47: invokevirtual #8
50: iload_0
51: invokevirtual #9
54: invokevirtual #10
57: invokevirtual #11
60: iload_1 //将局部变量表 1 位置的元素加载进操作栈(2)
61: ireturn //将操作栈顶元素返回(2)
-------------------try + finally 结束 ------------
------------------下面是 catch + finally,类似的 ------------
62: astore_1
63: iinc 0, 1
.......
.......
- 从我们的分析中可以看出来,finally 代码块中的内容始终会被执行,无论程序是否出现异常的原因就是,编译器会将 finally 块中的代码复制两份并分别添加在 try 和 catch 的后面。
可能有人会所疑惑,原本我们的 i 就被存储在局部变量表 0 位置,而最后 finally 中的代码也的确将 slot 0 位置填充了数值 10,可为什么最后程序依然返回的数值 2 呢?
- 仔细看字节码,你会发现在 return 语句返回之前,虚拟机会将待返回的值压入操作数栈,等待返回,即使 finally 语句块对 i 进行了修改,但是待返回的值已经确实的存在于操作数栈中了,所以不会影响程序返回结果。
4.3 面试题三
public static int test4(){
//finally 语句块中有 return 语句
int i = 1;
try{
i++;
System.out.println("try block, i = "+i);
return i;
}catch(Exception e){
i++;
System.out.println("catch block i = "+i);
return i;
}finally{
i++;
System.out.println("finally block i = "+i);
return i;
}
}
运行结果:
try block, i = 2
finally block i = 3
3
其实你从它的字节码指令去看整个过程,而不要单单四记它的执行过程。
你会发现程序最终会采用 finally 代码块中的 return 语句进行返回,而直接忽略 try 语句块中的 return 指令。
最后,对于异常的使用有一个不成文的约定:尽量在某个集中的位置进行统一处理,不要到处的使用 try-catch,否则会使得代码结构混乱不堪。
参考
Java 的异常处理机制
Java异常体系