本章主要介绍字节码实现的finally子句。包括相关指令以及这些指令的使用方式。此外,本章还介绍了Java源代码中finally子句所展示的一些令人惊讶的特性,并从字节码角度对这些特征进行了解释。
1、微型子例程
字节码中的finally子句表现的很像“微型子例程”。Java虚拟机在每个try语句块和与其相关的catch子句的结尾处都会“调用”finally子句的子例程。finally子句结束后(这里的结束指的是finally子句中最后一条语句正常执行完毕,不包括抛出异常,或执行return、continue、break等情况),隶属于这个finally子句的微型子例程执行“返回”操作。程序在第一次调用微型子例程的地方继续执行后面的语句。
Java方法与微型子例程使用不同的指令集。跳转到微型子例程的指令是jsr或者jsr_w,将返回地址压入栈。执行完毕后调用ret指令。ret指令并不会从栈中弹出返回地址,而是在子例程开始的时候将返回地址从栈顶取出存储在局部变量,ret指令从局部变量中取出。这是因为finally子句本身会抛出异常或者含有return、break、continue等语句。finally确保会执行到,即使try或者catch中有return等语句。
先看看下面的一道面试题:
int normal(){ try{ return 10; }finally{ return 20; } }
以上函数的返回结果时什么?
对于finally,我们通常的认识一般如下:
不管异常是否发生,它都会执行,但是看上面的例子,哪个return先执行?
其实际语义具体描述如下:
try中抛出异常后,如果存在捕获异常的过程,那么这个过程执行完后会执行finally;
try中没有异常发生,那么当try执行到结尾后,执行finally.
注意“try执行到结尾”是指在return之前。
所以normal()函数实际上返回的是20.
Java虚拟机规范中谈到,finally子句类似于一种微型子例程,我们可以通过查看class文件的字节码理解。
新建TryFinallyTest.java进行测试:
public class TryFinallyTest{ private static int normal(){ try{ return 10; }finally{ return 20; } } public static void main(String args[]){ System.out.println(normal()); } }
使用javap -c -private TryFinallyTest输出类汇编版本:
(关于以下的jvm指令的含义,可以参考《Java虚拟机规范》或者《Inside JVM》)
Compiled from "TryFinallyTest.java" public class TryFinallyTest extends java.lang.Object{ public TryFinallyTest(); Code: 0:aload_0 1:invokespecial#1; //Method java/lang/Object."<init>":()V 4:return private static int normal(); Code: 0:bipush10 2:istore_1 3:bipush20 5:ireturn 6:astore_2 7:bipush20 9:ireturn Exception table: from to target type 0 3 6 any 6 7 6 any //0-2 对应try子句,“3-5”及"7-9"表示finally子例程,而6表示编译器生成的catch处理过程的开端(这可以通过加粗的异常表看到)。 //注意我们没有看到关于return 10的任何信息,该语句已经被忽略了。 public static void main(java.lang.String[]); Code: 0:getstatic#2; //Field java/lang/System.out:Ljava/io/PrintStream; 3:invokestatic#3; //Method normal:()I 6:invokevirtual#4; //Method java/io/PrintStream.println:(I)V 9:return }
示例2:Surprise.java
class Surprise { static boolean surpriseTheProgrammer(boolean bVal) { while (bVal) { try { return true; } finally { break; } } return false; } }
相信经过例子1可以知道答案了。
对应的类汇编版本:
Compiled from "Surprise.java" class Surprise extends java.lang.Object{ Surprise(); Code: 0: aload_0 1: invokespecial #8; //Method java/lang/Object."<init>":()V 4: return static boolean surpriseTheProgrammer(boolean); Code: 0: iload_0 1: ifeq 8 4: goto 8 7: pop 8: iconst_0 9: ireturn Exception table: from to target type 4 7 7 any }
《Inside JVM》中提到finally子句往往通过jsr指令跳转,但是javap显示的结果并不是这样的,编译器做了一定的优化。
示例3:
Finally子句的一个令人困惑的特性:
private static int normal() { int a; try { a = 1; System.out.println("in try:set a to " + a); return a; } finally { a = 2; System.out.println("in finally:set a to " + a); } }
请问normal的返回值是多少,结果如下
in try:set a to 1 in finally:set a to 2 1
先看看javap的结果吧,
private static int normal(); Code: 0:iconst_1 1:istore_0 //对应a=1(现在栈中压入常量1,再弹出存储到局部变量_0即a中) 2:getstatic#2; //Field java/lang/System.out:Ljava/io/PrintStream; 5:new#3; //class java/lang/StringBuilder 8:dup 9:invokespecial#4; //Method java/lang/StringBuilder."<init>":()V 12:ldc#5; //String in try:set a to 14:invokevirtual#6; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 17:iload_0 18:invokevirtual#7; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 21:invokevirtual#8; //Method java/lang/StringBuilder.toString:()Ljava/lang/String; 24:invokevirtual#9; //Method java/io/PrintStream.println:(Ljava/lang/String;)V //9-24对应try块中的打印语句 27:iload_0 28:istore_1 //令人困惑的原因在这里-----编译器在try块中把变量a的值存在了标号为1的局部变量中,再跳到语句56我们就明白了,try中返回的是标号1的局部变量的值,而不是修改过的值。 29:iconst_2 30:istore_0 31:getstatic#2; //Field java/lang/System.out:Ljava/io/PrintStream; 34:new#3; //class java/lang/StringBuilder 37:dup 38:invokespecial#4; //Method java/lang/StringBuilder."<init>":()V 41:ldc#10; //String in finally:set a to 43:invokevirtual#6; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 46:iload_0 47:invokevirtual#7; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 50:invokevirtual#8; //Method java/lang/StringBuilder.toString:()Ljava/lang/String; 53:invokevirtual#9; //Method java/io/PrintStream.println:(Ljava/lang/String;)V //29-53对应finally块 56:iload_1 57:ireturn //56-57对应try中的return a. 58:astore_2 59:iconst_2 60:istore_0 //59-60对应finally中的a=2; 61:getstatic#2; //Field java/lang/System.out:Ljava/io/PrintStream; 64:new#3; //class java/lang/StringBuilder 67:dup 68:invokespecial#4; //Method java/lang/StringBuilder."<init>":()V 71:ldc#10; //String in finally:set a to 73:invokevirtual#6; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 76:iload_0 77:invokevirtual#7; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 80:invokevirtual#8; //Method java/lang/StringBuilder.toString:()Ljava/lang/String; 83:invokevirtual#9; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 86:aload_2 87:athrow Exception table: from to target type 0 29 58 any 58 59 58 any
假设上面方法修改的是对象的引用,结果也是类似的,两者的差别仅在于字节码中的操作码有所不同。
(一个是iload和istore,一个是astore和aload)
所以,如果在finally中修改了变量,而又需要返回该值,那么必须在finally块中添加返回语句。finally里面对result再赋值也不会影响栈顶的返回值,只会影响result的值。但如果在finally里面也加了return的话,这时候虚拟机栈又会把result的值copy到return的栈顶,这时候返回的就是finally里面重新赋值之后的result的值了