finally子句和try子句中return的先后关系
这个问题起因于java区基础版块的一篇帖子:
<a href="http://community.csdn.net/Expert/topic/3636/3636856.xml?temp=.9524347" target="_blank">http://community.csdn.net/Expert/topic/3636/3636856.xml?temp=.9524347</a>
下面是我从《深入Java虚拟机 2E》(中文版,曹晓钢 蒋靖译)中得到的解释
由于现学现卖,不免有大量摘抄的地方,望诸位谅解。
jsr指令是使java虚拟机跳转到微型子例程[注释1]的操作码,另外一条指令使jsr_w,后者支持比前者更长的操作数(4个字节长)。当java虚拟机遇到jsr或是jsr_w指令,它会把返回地址压入栈,然后从微型子例程的开始处继续执行。
微型子例程执行完毕后(这里指的是finally子句中最后一条语句正常执行完毕,不包括抛出异常,或执行return、continue、break等情况),将调用ret指令,ret指令的功能是执行从子例程中返回的操作。
你也许会认为,ret指令应当从栈中弹出返回地址,因为返回地址也已被jsr指令压入栈。不是这样的,ret指令并不会这样做。在每一个子例程的开始处,返回地址都从栈顶端弹出,并且存储在局部变量中,稍后,ret指令将会从这个局部变量中取出返回地址。这种对返回地址的不对称的工作方式是必要的,因为finally子句本身会抛出异常或者含有return、break、continue等语句。由于这些可能性的存在,这个被jsr指令压入栈的额外返回地址必须立即从栈中移除。因此,当finally子句通过break、continue、return或者抛出异常退出时,这个问题就不必再考虑了。
先看主帖里的示例代码(为了bytecode的清晰,去掉了无关的打印语句和捕获异常语句)
// Test1.java
public class Test1{
public static void main(String[] args){
System.out.print(tt());
}
public static int tt(){
int b = 23;
try{
return b = 88;
}
finally {
if(b > 25){
System.out.println("b > 25 : "+b);
}
}
}
}
//调用javap -c Test1后得到的字节码序列
Compiled from "Test1.java"
public class Test1 extends java.lang.Object{
public Test1();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
3: invokestatic #3; //Method tt:()I
6: invokevirtual #4; //Method java/io/PrintStream.print:(I)V
9: return
public static int tt();
Code:
0: bipush 23 // 将数据23转换为int类型,然后将其压入栈
2: istore_0 // 从栈中弹出int类型值,然后将其存到位置为0的局部变量中
// 执行int b = 23;
3: bipush 88 // 将数据88转换为int类型,然后将其压入栈
5: dup // 复制栈顶部的一个字,然后再将复制内容压入栈
// 这里是执行b = 88语句
6: istore_0 // 从栈中弹出int类型值,然后将其存到位置为0的局部变量中
// 这里存的是88,b = 88语句的执行后b的值
7: istore_1 // 从栈中弹出int类型值,然后将其存到位置为1的局部变量中
// 这里存的是88,b = 88语句的执行结果,将要被return语句返回的值
8: jsr 19 // 把返回地址压入栈,跳转至偏移量指定位置处执行分支操作
// 这里先将指令8的偏移地址压入栈,然后跳转到指令19,finally子句的开始
11: iload_1 // 将位置为1的int类型局部变量压入栈
// 将先前存到位置为1的局部变量中的返回值压入栈
12: ireturn // 从方法中返回int类型的数据
// 即try子句中最后的一步操作:return方法tt的的返回值
13: astore_2
14: jsr 19
17: aload_2
18: athrow
// 指令13-18是针对try子句产生异常的情况,这里不做分析
19: astore_3 // 从栈中弹出对象引用,然后将其存到位置为3的局部变量中
// finally子句的开始
// 这里弹出的是指令8(不发生异常的情况下)中压入栈的返回地址,原因在前面摘抄的文字中已经解释过了
20: iload_0 // 将位置为0的int类型局部变量压入栈
21: bipush 25 // 将数据25转换为int类型,然后将其压入栈
23: if_icmple 51
26: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
29: new #5; //class StringBuffer
32: dup
33: invokespecial #6; //Method java/lang/StringBuffer."<init>":()V
36: ldc #7; //String b > 25 :
38: invokevirtual #8; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
41: iload_0
42: invokevirtual #9; //Method java/lang/StringBuffer.append:(I)Ljava/lang/StringBuffer;
45: invokevirtual #10; //Method java/lang/StringBuffer.toString:()Ljava/lang/String;
48: invokevirtual #11; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
// 指令20-48执行if子句,略过,不做分析
51: ret 3 // 从子例程中返回到保存在局部变量3中的地址
// 呼应指令19,从finally子句转回到try子句
Exception table:
from to target type
3 11 13 any
13 17 13 any
}
------------------------------------------------------------------
上面的中文注释,直接对应在指令后面的是《深入java虚拟机》中的指令说明,其下的才是我所理解的行为。
可以看出,源代码中简单的一句return b = 88;语句,编译成bytecode后,对应了3-12条指令。其中指令8是一个分界点,指令3-7执行b = 88这个语句,且赋值运算后将b的值和方法的返回值分别存入到位置为0和1的局部变量中,指令11、12则从位置为1的局部变量中取出方法的返回值,并返回。指令8则是将自己的偏移地址压入栈,然后跳转到指令19,开始执行finally子句。
finally子句先将指令8的偏移地址弹出栈,并保存到位置为3的局部变量中,然后开始执行后面的语句。当后面的语句执行完毕,通过指令51,从位置为3的局部变量中取出指令8的偏移地址,然后返回执行指令8的后续指令(try子句中最终的return指令)。
(待续,明天再补上对finally子句中直接用写return 123;的理解)
注释:
1.字节码中的finally子句在方法内部的表现很像“微型子例程”,因此本文中的“微型子例程”特指finally子句。