问题
Java异常体系中,在try块或catch块内使用return时,可能会出现出乎开发人员预料的结果,引发问题的主要原因是对finally和return的执行顺序的不熟悉,本文将对相关代码进行反编译,分析其执行顺序。
先看下列一段代码:
public class Test {
public static void main(String[] args) {
System.out.println("main i = " + test());
}
public static int test(){
int i = 100;
try{
i = 200;
System.out.println("try i = " + i);
return i = 300;
}finally {
i = 400;
System.out.println("finally i = " + i);
}
}
}
执行结果如下:
try i = 200
finally i = 400
main i = 300
执行结果可能会不符合部分开发人员的预期,因为相信在开发人员学习异常体系的时候,都会听过finally在最后必定执行这句话,从执行结果来看finally块也的确被安排在最后执行,但却没能改变返回结果,引出了本篇博文想要深入探讨的问题,即finally块和return语句的执行顺序。
分析
try块/catch块有return
接下来对Test测试类进行javap反编译结果分析,在分析前,先简单对方法进行简化,以使得反编译结果聚焦在要讨论的问题上,简化后代码如下:
public class Test {
public static void main(String[] args) {
test();
}
public static int test(){
int i = 100;
try{
i = 200;
return i = 300;
}finally {
i = 400;
}
}
}
test()方法反编译结果如下,结果中会添加注释:
public static int test();
Code:
0: bipush 100 //将100送入操作数栈顶
2: istore_0 //将栈顶int型数值(100)存入第0个本地变量
3: sipush 200 //将200送入操作数栈顶
6: istore_0 //将栈顶int型数值(200)存入第0个本地变量 此时第0个本地变量被覆盖 现值为200
7: sipush 300 //将300送入操作数栈顶
10: dup //复制栈顶数值并将复制值压入栈顶 此时栈内两个300
11: istore_0 //将栈顶int型数值(300)存入第0个本地变量 此时第0个本地变量被覆盖 现值为300
12: istore_1 //将栈顶int型数值(300)存入第1个本地变量
13: sipush 400 //将400送入操作数栈顶
16: istore_0 //将栈顶int型数值(400)存入第0个本地变量 此时第0个本地变量被覆盖 现值为400
17: iload_1 //将第1个本地变量(300)送到栈顶
18: ireturn //从当前方法返回int
19: astore_2
20: sipush 400 //将400送入操作数栈顶
23: istore_0 //将栈顶int型数值(400)存入第0个本地变量
24: aload_2
25: athrow
结果中出现过的JVM指令说明如下,说明中所提及的栈均为操作数栈:
指令 | 说明 |
bipush | 将单字节的常量值(-128~127)推送至栈顶 |
sipush | 将一个短整型常量值(-32768~32767)推送至栈顶 |
istore_x | 将栈顶int型数值存入第x个本地变量 |
iload_x | 将指定的第x个int型本地变量推送至栈顶 |
astore_x | 将栈顶引用型数值存入第x个本地变量 |
aload_x | 将第x个引用类型本地变量推送至栈顶 |
ireturn | 从当前方法返回int |
athrow | 将栈顶的异常抛出 |
dup | 复制栈顶数值并将复制值压入栈顶 |
根据上文反编译结果分析可见指令流程大体如下:
- 执行return前代码:0~6行执行i = 100; i =200;
- 执行return语句:注意是此处是执行return语句,并非返回操作,7~12行执行i = 300;
- 执行finally语句:13~16行执行i = 400;
- 执行return返回操作:17~18行返回栈顶int型数值,将数据送至栈顶时,推送的本地变量是return语句最后保存的本地变量,而非finally块内最后保存的本地变量。
至此可以清楚的弄清,finally和return的执行顺序是
- return语句执行。
- finally执行。
- return执行返回。finally内修改return语句将要返回的对象并不实际影响返回结果。
catch块中存在return时情况与try块完全一致,finally的内容也无法实际影响catch块中的return返回。
当出现try块和catch块都有return语句时,程序照常运行,出现异常时执行catch,无异常执行try块。
try块和finally块都有return
现将代码修改如下,以模拟标题所述情况:
public class Test {
public static void main(String[] args) {
test();
}
public static int test(){
int i = 100;
try{
i = 200;
return i = 300;
}finally {
i = 400;
return i;
}
}
}
上面一段代码test()方法反编译结果如下,重复部分不做冗余注释:
public static int test();
Code:
0: bipush 100
2: istore_0
3: sipush 200
6: istore_0
7: sipush 300
10: dup
11: istore_0 //try块中的300保存在本地变量0
12: istore_1 //try块中的300保存在本地变量1
13: sipush 400
16: istore_0 //finally块中的400保存在本地变量0 覆盖300
17: iload_0 //将本地变量0(400) 送入栈顶
18: ireturn //栈顶返回
19: astore_2
20: sipush 400
23: istore_0
24: iload_0
25: ireturn
通过反编译结果可见17行在返回之前从送到操作数栈顶的变量是finally中最后修改过的值。
由此可以得出结论,当try/catch块和finally块中同时存在return语句时,实际执行的是finally块中的return语句。
相信仔细阅读的读者在以上的反编译结果也会发现一个问题,就是为什么看似finally块在字节码中执行了两次,尤其是为什么会有两个ireturn指令。对此笔者也是深受困扰,四下搜索也没有结果,如若有读者了解还请赐教纠正。
总结
文章验证后结论如下:实际执行时,如果finally中没有return,则执行顺序如下:
- 执行return语句。
- 执行finally。
- 执行返回操作,finally对于返回无影响。
如finally中有return,则最终一定执行finally中的return语句。