前言
我们都知道 try catch finally 语法快 finally的代码一定会执行,但是其底层的原理知道么?本文直面底层看下 为什么finnaly 语法块不管是正常情况下还是 抛出异常情况下都会执行
一、没有return 的 try catch finally
我们先看一个简单的示例:
//java代码
public static void main(String[] args) {
try {
int a = 1/1;// int a = 1/0
System.out.println("正常执行");
}catch (Exception e){
System.out.println("Exception");
}finally {
System.out.println("这里执行了");
}
}
上述代码很简单,当 int a = 1/1 时 输出了 “正常执行” 、“这里执行了”,try 和 finally 代码块执行了
当 int a = 1/0 时 输出了 “Exception”、“这里执行了”,catch 和 finally 的代码块都执行了
接下来我们看下字节码
//java 代码 int a = 1/1 字节码
public static void main(java.lang.String[]);
Code:
//0- 9行 执行try 代码块
0: iconst_1
1: istore_1
2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
5: ldc #3 // String 正常执行
7: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
//10-18 这里执行的是 finally 的代码块,然后跳转至 52行 return
10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
13: ldc #5 // String 这里执行了
15: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
18: goto 52
//21-30从下面异常表第一行 可知 如果0-9行抛出异常 会跳转至21行
//存储抛出的异常 输出 “Exception”
21: astore_1
22: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
25: ldc #7 // String Exception
27: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
//30-38 这里执行的是 finally 的代码块 然后跳转至 52行 return
30: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
33: ldc #5 // String 这里执行了
35: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
38: goto 52
//这里对应的是 异常表的第2 和 第三行 也就是 0-9行抛出了非 Exception的异常 或者 21-29行 抛出了任何异常 会跳转至 41行处理
41: astore_2 //存储抛出的异常到局部变量表
//输出"这里执行了"
42: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
45: ldc #5 // String 这里执行了
47: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
50: aload_2 //取出异常 并抛出
51: athrow
52: return
Exception table:
from to target type
0 10 21 Class java/lang/Exception
0 10 41 any
21 30 41 any
首先我们看到下面多了一个 Exception table: 有from、to、target、type
每一行的意义:from/to 表示 区间行数,from包含,to 不包含; target 和 type 表示 如果异常类型是 type 的话 跳转至 target 所在的行数执行
我们以第一行 为例,也就是 在 字节码的第 0 到 9 行(包含),如果执行期间有异常且是type 为 java/lang/Exception 则跳转至21行执行;any 表示除了Exception 外的其他异常。
经过上述字节码分析(看注释),try catch finally 会生成异常表来辅助代码执行,而确保finally 一定执行 是在 try catch 代码块内都复制了一份 finally的代码块。
二、包含return 的 try catch finnaly
上部分分析了 try-catch -finnaly 能确保 finally 代码块一定会执行的原理,那 如果 有返回值呢,会返回什么?
我们开看下示例代码:
public static int testReturn(){
try {
int a = 1/0; //int a = 1/1;
return 0;
}catch (Exception e){
return 1;
}finally {
return 2;
}
}
public static void main(String[] args) {
System.out.println(testReturn());
}
上述代码的测试结果无论会不会抛异常,输出结果都是 2.
那我们看下字节码只看下 testReturn 的:
public static int testReturn();
Code:
//0-7 对应的 try 代码块,加入了finnaly代码块
0: iconst_1 //将1 加载到操作数栈
1: iconst_0 //将0 加载到操作数栈
2: idiv //计算 1/0
3: istore_0 //存储计算结果
4: iconst_0 //将0 加载到栈顶
5: istore_1 //将 0 存入局部变量表 但未返回
6: iconst_2 //将2 加载到栈顶
7: ireturn // 返回
//8-12 对应 catch代码块 加入了finally 代码块
8: astore_0 //存储抛出的异常
9: iconst_1 //将1 加载到栈顶
10: istore_1 //将1存储到局部变量表 但未返回
//11-12行
11: iconst_2 //将2 加载到栈顶
12: ireturn //返回 2
//finally代码块
13: astore_2
14: iconst_2
15: ireturn
Exception table:
from to target type
0 6 8 Class java/lang/Exception //0-5行如果有异常跳转至 第8行
0 6 13 any //0-5行如果其他异常 跳转至 13行
8 11 13 any //8-10行如果有其他异常 跳转至 13行
可以看到 为了 确保 finally 代码块一定执行,在 try-catch 中返回的时候只是做了存储到局部变量表,真正返回的还是 复制到各个代码块的finally 代码块,所以返回的 都是 2.(试想下如果返回的不是 2 那不就代表了 finally 代码块不一定会执行 )
如果要在try-catch-finally 中有返回值,请谨慎使用,因为 finally 一定会执行,如果finally 有返回值,则一定会执行 finally 的返回值;如果需要 try 和 catch 中的返回值,则finally 中的return语句可以不写。
三、try--with--resource的原理
try-with-resources 是 JDK 7 中一个新的异常处理机制,它能够很容易地关闭在 try-catch 语句块中使用的资源。所谓的资源(resource)是指在程序完成后,必须关闭的对象。try-with-resources 语句确保了每个资源在语句结束时关闭。所有实现了 java.lang.AutoCloseable 接口(实现了 java.io.Closeable 的所有对象)都可以用上述用法。那么保证资源对象一定关闭,是实现了finally 么 ?
我们看下如下示例:
void readFile() {
try (
FileReader fr = new FileReader("d:/input.txt");
BufferedReader br = new BufferedReader(fr)
) {
String s = "";
while ((s = br.readLine()) != null) {
System.out.println(s);
}
} catch (Exception e) {
e.printStackTrace();
}
}
直接看下字节码:
void readFile();
Code:
/
* 生成 FileReader 放入 局部变量表 位置 1
* 将 null 放入 局部变量表 位置 2
* 生成 BufferedReader 放入 局部变量表 位置 3
* 将 null 放入 局部变量表 位置 4
* 将字符串s 初始值 “” 放入 局部变量表 位置 5
*/
0: new #2 // class java/io/FileReader
3: dup
4: ldc #3 // String d:/input.txt
6: invokespecial #4 // Method java/io/FileReader."<init>":(Ljava/lang/String;)V
9: astore_1
10: aconst_null
11: astore_2
12: new #5 // class java/io/BufferedReader
15: dup
16: aload_1
17: invokespecial #6 // Method java/io/BufferedReader."<init>":(Ljava/io/Reader;)V
20: astore_3
21: aconst_null
22: astore 4
24: ldc #7 // String
26: astore 5
/*
*加载 BufferedReader 并 调用 readLine 赋值给 s
**/
28: aload_3
29: invokevirtual #8 // Method java/io/BufferedReader.readLine:()Ljava/lang/String;
32: dup
33: astore 5
/*
* 判断 s 是否为null 不为null 则执行完后 继续跳转至 28行,如果为空 跳转至 49行
*/
35: ifnull 49
38: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
41: aload 5
43: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
46: goto 28
/*
*判断 BufferedReader是否为null 如果为null 跳转至 130行,不为null 局部变量表第四个位置是否为null
* 如果 为null 跳转至 77行 ,否则执行 BufferedReader close 跳转至 130行
*/
49: aload_3
50: ifnull 130
53: aload 4
55: ifnull 77
58: aload_3
59: invokevirtual #11 // Method java/io/BufferedReader.close:()V
62: goto 130
/**
*执行 58-61行即 BufferedReader.close 如果抛出异常 会进入此
* 存储抛出的异常 到局部变量表位置 5这里 原位置的 s 已经没用了覆盖成了异常
* 取出 变量表 4 和 5 位置的异常,调用 Throwable.addSuppressed 拼接异常
**/
65: astore 5
67: aload 4
69: aload 5
71: invokevirtual #13 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
74: goto 130
//调用 BufferedReader close 跳转至 130行
77: aload_3
78: invokevirtual #11 // Method java/io/BufferedReader.close:()V
81: goto 130
/**
*执行 24-48 行 如果抛出Throwable 异常 会进入此
* 存储抛出的异常 到局部变量表位置 5这里 原位置的 s 已经没用了覆盖成了异常
* 位置的异常,调用 Throwable.addSuppressed 拼接异常
**/
84: astore 5
86: aload 5
88: astore 4
90: aload 5
92: athrow
/*
*存储 24-48 84-95 抛出的非 throwable 异常 尝试调用 BufferedReader.close
*/
93: astore 6
95: aload_3
96: ifnull 127
99: aload 4
101: ifnull 123
104: aload_3
105: invokevirtual #11 // Method java/io/BufferedReader.close:()V
108: goto 127
/*
*存储 104 -108 BufferedReader.close 抛出的异常 并拼接异常
*/
111: astore 7
113: aload 4
115: aload 7
117: invokevirtual #13 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
120: goto 127
123: aload_3
124: invokevirtual #11 // Method java/io/BufferedReader.close:()V
//抛出异常
127: aload 6
129: athrow
/*
*判断 FileReader 是否为null 如果为null 跳转至 201行,不为null 局部变量表第2个位置是否为null
* 如果 为null 跳转至 154 行 ,否则执行 BufferedReader close 跳转至 201 行
*/
130: aload_1
131: ifnull 201
134: aload_2
135: ifnull 154
138: aload_1
139: invokevirtual #14 // Method java/io/FileReader.close:()V
142: goto 201
/*
*存储 138 -144 FileReader.close 抛出的异常 并拼接异常
*/
145: astore_3
146: aload_2
147: aload_3
148: invokevirtual #13 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
151: goto 201
154: aload_1
155: invokevirtual #14 // Method java/io/FileReader.close:()V
158: goto 201
/*
*存储 12 -129 行 抛出的Throwable异常
*/
161: astore_3
162: aload_3
163: astore_2
164: aload_3
165: athrow
/*
*存储 12 -129 行 抛出的非 Throwable异常
*/
166: astore 8
168: aload_1
169: ifnull 198
172: aload_2
173: ifnull 194
176: aload_1
177: invokevirtual #14 // Method java/io/FileReader.close:()V
180: goto 198
183: astore 9
185: aload_2
186: aload 9
188: invokevirtual #13 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
191: goto 198
194: aload_1
195: invokevirtual #14 // Method java/io/FileReader.close:()V
198: aload 8
200: athrow
/*
* 跳转至 209行
*/
201: goto 209
/*
* 只有异常表最后一行 0-200行抛出异常时会进入这里,未捕获的异常输出
*/
204: astore_1
205: aload_1
206: invokevirtual #16 // Method java/lang/Exception.printStackTrace:()V
/*
* 执行结束
*/
209: return
Exception table:
from to target type
58 62 65 Class java/lang/Throwable
24 49 84 Class java/lang/Throwable
24 49 93 any
104 108 111 Class java/lang/Throwable
84 95 93 any
138 142 145 Class java/lang/Throwable
12 130 161 Class java/lang/Throwable
12 130 166 any
176 180 183 Class java/lang/Throwable
161 168 166 any
0 201 204 Class java/lang/Exception
这么长的字节码一看就傻眼了,莫慌 我们有策略的分析下。
策略1: 我们按照 异常表的target 行数,将 字节码分成几块
策略2: 将字节码中 有if 判断跳转的 和 goto 跳转的 分块
分完块后我们 来看下每块的内容,逐个分析
正常的执行流程:
35行判断 s为null 进入 49(50)行,
49(50) 判断 BufferedReader 不为null,且没有异常,执行 close 进入 130行
130-139FileReader 不为null 且没有异常 执行 close 进入 201行 然后进入 209结束
异常执行流程:
我们挑一个流程 假设 24-49 抛出了 Throwable,我们看下流程:
代码会进入 84行
84-108: 处理异常,如果 BufferedReader 不为null 执行 BufferedReader.close,并进入 127行
127-142:处理异常 并执行 FileReader.close 并进入 201 行 然后进入 209行 结束
小结:
- 第一,try-with-resource 语法并不是简单的在 finally 里中加入了
closable.close()
方法,因为 finally 中的 close 方法如果抛出了异常会淹没真正的异常; - 第二,引入了 suppressed 异常的概念,能抛出真正的异常,且会调用 addSuppressed 附带上 suppressed 的异常