字节码层面分析 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
。