字节码层面分析 try-catch-finally 中的 return 问题
结论
finally中的代码总会被执行(Java语言规范规定的)。- 如果
try、catch中有return,那么:finally中无return,当返回值的类型是引用类型(可变类)时,返回值会受到finally中代码的影响。finally中有return,会直接在finally中退出,导致try、catch中的return失效。
测试代码
public class TestReturn {
public static void main(String[] args) {
}
private static int testReturn1() {
int i = 0;
try {
i++;
return i; // 1
} catch (Exception e) {
i++;
} finally {
i++;
}
return i;
}
private Integer testReturn2() {
Integer i = 0;
try {
i++;
return i; // 2
} catch (Exception e) {
i++;
} finally {
i++;
}
return i;
}
private int testReturn3() {
int i = 0;
try {
i++;
int x = i / 0 ;
} catch (Exception e) {
i++;
return i; // 2
} finally {
i++;
}
return i;
}
private int testReturn4() {
int i = 0;
try {
i++;
return i;
} catch (Exception e) {
i++;
return i;
} finally {
i++;
return i; // 2
}
}
}
执行一下代码以便生成 TestReturn.class。
反编译
方法一:打开 cmd,并将窗口最大化(避免打印的反编译信息换行),进入 TestReturn.class 所在目录,执行命令 javap -v -p TestReturn 将字节码反编译成助记符。
方法二:借助于 IDEA 插件 jclasslib 查看字节码。
基本类型
以 testReturn1() 方法为例来分析字节码,这里使用 jclasslib 查看字节码。
字节码
Code 中的字节码页签就是字节码反编译后的字节码行号和助记符。

异常表
如果2~7 行出现 Exception 则跳转到 12 行进行处理,否则跳转到 22 行进行处理。

行号表
Code 中的 LineNumberTable 记录的是 PC(程序计数器,也就是字节码行号指示器)对应的 Java 代码行号。

根据这个映射表,我们可以把字节码跟 Java 代码联系起来。

由上图可知,finally 一定会执行的语义是通过在 try 和 catch 后面添加 finally 的字节码实现的。
逐行分析
0 iconst_0
当 int 取值 -1~5 时,JVM 采用 iconst 指令将常量压入栈中。

1 istore_0
弹出栈顶元素,并保存到局部变量表的 0 号槽。

2 iinc 0 by 1
0 号槽的变量加 1。

5 iload_0
将局部变量表 0 号槽的变量入栈,作为暂定的返回值。

6 istore_1
弹出栈顶元素,并保存到局部变量表的 1 号槽,即保存暂定的返回值。

7 iinc 0 by 1
0 号槽的变量加 1。

10 iload_1
将局部变量表 1 号槽的变量入栈。

11 ireturn
返回栈顶的值。
对于本代码,执行到这里就结束了,总体流程为:
try → finally → try 中的 retrun。

12 astore_1
将异常对象 e 保存到局部变量表的 1 号槽(覆盖掉暂定的返回值)。
由于2~7 行出现 Exception 都会跳转到 12 行进行处理,所以此时的状态不好确定,后面的就不分析了。
引用类型
这里直接贴出 testReturn2() 跟字节码关联后的图。

逐行分析
0 iconst_0
当 int 取值 -1~5 时,JVM 采用 iconst 指令将常量压入栈中。

1 invokestatic #3 <java/lang/Integer.valueOf>
调用编号为 3 的类方法,也就是 Integer.valueOf,将栈顶的基本类型转换为引用类型(以后缀 I 标识)。

4 astore_1
将栈顶的引用类型保存到局部变量表的 1 号槽。

5 aload_1
将局部变量表 1 号槽的引用类型压入栈中。

6 astore_2
将栈顶的引用类型保存到局部变量表的 2 号槽。
注意:此时的两个引用指向同一个对象。

7 aload_1
将局部变量表 1 号槽的引用类型压入栈中。

8 invokevirtual #4 <java/lang/Integer.intValue>
调用编号为 4 的实例方法,也就是 Integer.intValue, 将栈顶的引用类型转换为基本类型。

11 iconst_1
将常量 1 压入栈中。

12 iadd
弹出栈顶两 int 类型数,相加后结果入栈。

13 invokestatic #3 <java/lang/Integer.valueOf>
将栈顶的基本类型转换为引用类型。

16 dup
复制栈顶元素,并再次入栈,即此时栈中有两个相同的引用。
这两个引用与变量槽中的不同,用浅蓝色区分。

17 astore_1
将栈顶的引用类型覆盖到局部变量表的 1 号槽。

18 astore_3
将栈顶的引用类型保存到局部变量表的 3 号槽。

19 aload_2
将局部变量表 2 号槽的引用类型压入栈中。

20 pop
从栈顶弹出一个字长的元素,即弹出栈顶。

21 aload_1
将局部变量表 1 号槽的引用类型压入栈中。

22 astore_2
将栈顶的引用类型覆盖到局部变量表的 2 号槽。

23 aload_1
将局部变量表 1 号槽的引用类型压入栈中,作为暂定的返回值。

24 astore_3
将栈顶的引用类型覆盖到局部变量表的 3 号槽。

25 aload_1
将局部变量表 1 号槽的引用类型压入栈中。

26 invokevirtual #4 <java/lang/Integer.intValue>
调用编号为 4 的实例方法,也就是 Integer.intValue, 将栈顶的引用类型转换为基本类型。

29 iconst_1
将常量 1 压入栈中。

30 iadd
弹出栈顶两 int 类型数,相加后结果入栈。

31 invokestatic #3 <java/lang/Integer.valueOf>
将栈顶的基本类型转换为引用类型(以绿色标识)。

34 dup
复制栈顶元素,并再次入栈,即此时栈中有两个相同的引用。

35 astore_1
将栈顶的引用类型覆盖到局部变量表的 1 号槽。

36 astore 4
将栈顶的引用类型保存到局部变量表的 4 号槽。

38 aload_3
将局部变量表 3 号槽的引用类型压入栈中。

39 pop
从栈顶弹出一个字长的元素,即弹出栈顶。

40 aload_2
将局部变量表 2 号槽的引用类型压入栈中。

41 areturn
返回栈顶引用,这样返回的就是 1。

浙公网安备 33010602011771号