1. 摘要
Java 的 try-catch 写法我们经常用到,按照固定的方式我们可以很容易捕获和处理异常:
try {
....
} catch( e ) {
...
} finally {}
复制代码
但是,我们会在某些写法上出现误区,这里将这些误区称为陷阱,本文会具体分析这些陷阱的产生原理,避免大家将来入坑。
2.例子1
//demo1
public static int testTryCatch() {
try {
int i = 1/0;// step1
} catch(Exception e) { //2
System.out.println("hello!");
return 1;
}finally{
return 2;
}
}
复制代码
在 demo1
代码的 step1
位置,发生了个除以 0
的异常,按照代码的逻辑将走到 catch
代码块。catch
代码块执行了一个 return
指令返回了字面量 1
。但是在 finally
语句块中也执行了一个 return
指令,那么最后函数将返回那个值呢?
我们执行该函数将得到结果:
>"hello!"
>2
复制代码
也就是执行finally
语句块中的指令,这是为什么呢?我们来看下这段代码编译出来的字节码:
Code:
stack=2, locals=1, args_size=0
0: iconst_1
1: iconst_0
2: idiv
3: istore_0
4: goto 20
7: astore_0
8: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
11: ldc #22 // String hello!
13: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: goto 20
19: pop
20: iconst_2
21: ireturn
Exception table:
from to target type
0 4 7 Class java/lang/Exception
0 19 19 any
复制代码
上面 JVM
字节码中 0~2
行代表 try
语句块, 当执行到 idiv
除法指令的时候,出现了异常。这时候我们需要查一下我们的异常向量表,异常向量表包含两条数据:
- 指令 0 到 4 行,捕获
Exception
类型数据,跳转到第 7 条指令 - 指令 0 到 19 行,捕获任何类型的
Throwable
,跳转到 19 语句块
- 我们执行
try
语句块中的idiv
指令位于第 2 行,属于区间 [0,4] ,因此,当该语句发生异常的时候将执行向量表的第一条记录,跳转到代码 7 。 - 代码 7 开始其实对应的是 catch 语句块, 这里将调用
astore_0
,将一个引用对象(实际上是一个Exception
对象)存入局部变量表,然后执行System.out.println
指令。之后,并没有按照我们的代码写法直接执行return 1
,return
指令被舍弃,而是跳转到 代码行 20 的位置,也就是finally
代码块 - 代码 20 行将存入一个字面量 2,然后执行
ireturn
指令返回。
因此,实际上编译器在编译我们的
try-catch
语法代码的时候,当发现我们的finally
中有return
语句的时候,将舍弃掉catch
代码块中的ireturn
指令
3.例子2
//code 2
public static int testFunc(MyObj obj) {
try {
int i = 1/0;
return 10;
} catch(Exception e) { //2
obj.v= 2;
return obj.v;
}finally{
obj.v= 3;
}
}
复制代码
通过我们对 例子1 的分析,我们知道 finally
语句将会插入到 catch
语句的 return
代码之前。那么按照这种结论我们的 例子2 就可以转化为:
catch(Exception e) {
obj.v = 2;
obj.v= 3;//finally 语句块
return obj.v;
}
复制代码
也就是说 例子2 中函数的结果应该是: 3 。 但是,结论总是事与愿违的,实际上返回的是 2。我们还是看下它编译的字节码:
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: iconst_0
2: idiv //异常
3: istore_1
4: aload_0
5: iconst_3
6: putfield #16 // Field david/support/Demos$MyObj.v:I
9: bipush 10
11: ireturn //catch 代码块
12: astore_1
13: aload_0
14: iconst_2
15: putfield #16 // Field david/support/Demos$MyObj.v:I
18: aload_0
19: getfield #16 // Field david/support/Demos$MyObj.v:I
22: istore_3
23: aload_0
24: iconst_3
25: putfield #16 // Field david/support/Demos$MyObj.v:I
28: iload_3
29: ireturn
30: astore_2
31: aload_0
32: iconst_3
33: putfield #16 // Field david/support/Demos$MyObj.v:I
36: aload_2
37: athrow
Exception table:
from to target type
0 4 12 Class java/lang/Exception
0 4 30 any
12 23 30 any
复制代码
- 代码执行到
try
语句块中的除法指令,对应代码第 2 行的时候发生的异常。 对应第 0 条向量表,跳转到 12 行catch
语句块 - 13 到 15 行,将字面量 2 存入局部变量
Demos$MyObj
对象的v
属性中 - 18 到 22 行,将局部变量
Demos$MyObj
对象的v
属性,存入到索引 [3] 的局部变量中 - 23 到 25 行,就是被插入的
finally
指令块,将对象的v
属性变成 3 - 28 到 29 行,将取出索引 [3] 的局部变量并返回
从上面的编译的代码我们可以看到,
例子2
中,finally
语句块中并没有return
指令的时候,编译器将会先把结果存入一个临时变量(上面的索引 [3] 局部变量)中,然后执行完finally
语句后,读取临时变量中的值,执行return
语句,并且,此时对象中的属性也因为执行了finally语句,而变成了 3 。