finally一直是java笔试中出现概率较高的知识点,我之前在面试的时候碰到也是很迷糊,今天就通过字节码来看finally最后究竟是怎样执行的,只要明白了原理,面试自然迎刃而解。阅读本文需要耐心,就算不懂字节码,多看看也就明白了,等哪天有空写一篇字节码入门的教程。废话不多说,进入正题。
1.try...catch的机制--异常表
java中的finally是不可以单独使用的,只能结合try使用,所以有必要先弄明白try...catch的原理。
源代码如下
public static void tryCatch(){
try {
test();
} catch (Exception e){
e.printStackTrace();
}
}
使用命令javap -v -p Test.class反编译得到方法的字节码
public static void tryCatch();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: invokestatic #5 // Method test:()V
3: goto 11
6: astore_0
7: aload_0
8: invokevirtual #7 // Method java/lang/Exception.printStackTrace:()V
11: return
Exception table:
from to target type
0 3 6 Class java/lang/Exception
从字节码可以看出,方法tryCatch有一个异常表(Exception table),异常表解释为,从位置from的指令开始,此处from=0,表示Code中的偏移量为0的指令,也就是invokestatic #5这个指令,到位置to(不包括to)的指令为止,这段指令执行期间如果发生异常且异常的类型type为Class java.lang.Exception,程序就跳转到target处指令,这里target=6处指令是astore_0。异常表的一行数据,表示一个catch块,这里只有一个行,也就是只有一个catch块。
下面让我逐行解释一下该方法的指令
偏移量0 invokestatic #5,该指令则是调用静态方法test()。
偏移量3 goto 11,如果上一条指令发生异常,就执行该条指令,直接跳转到偏移量11的指令。
偏移量6 astore_0,如果第一条指令发生异常,根据异常表,程序就会跳转到这条指令,6~8三条指令就是catch块代码对应的指令了,astore_0指令是将第一条指令的异常对象存储到局部变量表slot=1的位置。
偏移量7 aload_0,将局部变量表slot=1的位置异常对象压入操作数栈。
偏移量8 invokevirtual #7,调用java.lang.Excetion.printStackTrace()方法,调用对象就是上一个指令压入操作数栈的异常对象。
偏移量9 return,不管是从goto 11这条指令跳转到这的,还是发生了异常,执行了catch块代码后到这的,方法最后需要执行的都是return指令,方法结束。
2.finally机制--复制finally代码块到每个分支
做java的都知道或者听说过finally块一定会执行,那么java是如何保证finally块一定会执行的呢?直觉上是不是觉得通过跳转来实现的呢?
看源代码如下
public static void func1(){
try{
test();
}catch (Exception e){
test1();
}finally {
test2();
}
}
还是使用命令javap -v -p Test.class反编译得到方法的字节码如下
public static void func1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=0
0: invokestatic #5 // Method test:()V
3: invokestatic #8 // Method test2:()V
6: goto 25
9: astore_0
10: invokestatic #9 // Method test1:()V
13: invokestatic #8 // Method test2:()V
16: goto 25
19: astore_1
20: invokestatic #8 // Method test2:()V
23: aload_1
24: athrow
25: return
Exception table:
from to target type
0 3 9 Class java/lang/Exception
0 3 19 any
9 13 19 any
先看最后异常表,结合刚刚讲解的异常表知识,我们可以知道func1方法有三个catch块,但是我们的代码里明明只有一个catch块啊?解释就是编译器添加了两个catch块。第一行表示指令0发生Exception类型异常跳转到指令9。第二行表示指令0发生任何类型异常(也就是Throwable类型)跳转到指令19,前提是前面没有捕获该异常。第三行表示指令9~10发生任何类型异常则跳转到指令19。
现在来看方法字节码指令
0: invokestatic #5 调用静态方法test(),结合异常表可知,test若发生Exception类型异常,程序跳转到指令9,若发生Throwable类型异常,程序跳转到指令19,若没有发生异常,则顺序往下执行。
3:invokestatic #8 调用静态方法test2()。这是在try块中调用test2()。
6:goto 25 跳转到25指令return,将会结束方法。
9:astore_0 将异常对象存储到局部变量表slot=0的位置
10:invokestatic #9 调用静态方法test1(),这是第一个catch块的代码
13:invokestatic #8 调用静态方法test2()。这是在第一个catch块中调用test2()。
16:goto 25 同6。
19:astore_1将异常对象存储到局部变量表slot=1的位置,这是第二、三个catch块。
20: invokestatic #8 调用test2()。
23~24:将异常重新抛出。
25: return 方法结束。
通过解析字节码,可以知道编译后的func1相当于这样
public static void func1() {
try {
test();
test2();//编译器添加的finally块代码
} catch (Exception e){
try {
test1();
test2();//编译器添加的finally块代码
}catch (Throwable t){//编译器添加的Throwable类型的catch分支
test2();//添加这个分支的目的就是为了确保test1()方法发生异常后finally块还会被执行
throw t;//执行完test2后,将异常对象原样抛出
}
} catch (Throwable t){//这个也是编译器添加的Throwable类型的catch分支,目的还是为了确保finally块一定执行
test2();//编译器复制的finally块代码
throw t;//抛出异常
}
}
至此,我们可以回答这节开始的两个问题了,编译器将finally块代码复制到每个分支,并且编译器还会为每个分支添加一个Throwable类型catch分支,然后在这个catch分支里,先添加finally块的代码,再将异常对象原样抛出。finally是通过复制代码块到每个分支最后,而不是在每个分支最后跳转到finally块。
3.当finally遇到return,return的表现会有什么不一样呢?
我们先单独看return的字节码是怎样的,下面是源代码
public static int returnFunc(){
return 1;
}
javap反编译后的字节码指令如下
public static int returnFunc();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_1
1: ireturn
总共就两个指令,iconst_1,将int型常量1压入操作数栈,ireturn,将操作数栈顶的int型变量返回并结束方法。所以,通常来说代码return i 被编译为两个指令,第一个指令,将需要返回的值压入栈顶,第二个指令,将栈顶变量作为方法返回值并结束方法。
然后让我们来看finally和return一起使用时,return会有什么不一样呢?源代码如下,这也是从一个经典的面试题。朋友可以先猜一下返回值是什么再往下看。
public static int func2() {
int i = 0;
try {
return i;
} catch (Exception e){
return i;
} finally {
i++;
}
}
javap反编译后的字节码如下
public static int func2();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=0
0: iconst_0
1: istore_0
2: iload_0
3: istore_1
4: iinc 0, 1
7: iload_1
8: ireturn
9: astore_1
10: iload_0
11: istore_2
12: iinc 0, 1
15: iload_2
16: ireturn
17: astore_3
18: iinc 0, 1
21: aload_3
22: athrow
Exception table:
from to target type
2 4 9 Class java/lang/Exception
2 4 17 any
9 12 17 any
0~1: 两个指令为局部变量i赋值0,也就是int i=0。
2~3: iload_0将局部变量i的值压入栈顶,istore_1将栈顶变量存储到局部变量表slot=1的位置。这是代码return i对应的指令。哎,,,这里好像跟上一个例子中的return 1表现不一样,上一个例子中,return 1是将常量压入栈顶,然后返回返回1并结束方法,而这里return i将i的值压入栈顶后,并没有返回1然后结束方法,而是将栈顶变量又存到了局部变量表中,而且和原来局部变量i的位置不一样,这里的不一样就是因为finally,从这里可以看到finally将原来return的两个指令分成了多个指令。
4:innc 0,1 就是对局部变量表中slot=0的局部变量,也就是变量i,自增1,这对应着finally的代码i++。
7~8:iload_1 将局部变量表中slot=1的局部变量压入栈顶,这里并不是局部变量i,变量i在局部变量表中slot=0的位置,slot=1的位置是指令3存储的值,也就是try中return准备返回的值,ireturn则是以栈顶变量作为返回值并结束方法。
如果不发生异常,方法就到此结束了。剩下的代码就和上面finally的例子一样,就不另行分析了。
由此可知,当return后还有finally代码需要执行,return拆分成四个指令,前两个指令,将原本需要返回的变量存储为一个局部变量,当执行完finally代码后,再由后两个指令,将刚刚存储的局部变量加载到栈顶并返回。所以方法返回值0。
伪代码如下
int tmp = returnValue;//return i在执行finally代码前会先将i的存为一个临时局部变量
doFinally...;//执行finally块代码
return tmp;//执行完finally后,return i会取出刚刚存储的局部变量并返回
4.finally遇到return的另一种情况
再来看finally和return相遇的另一种情况,finally中有return语句。这同样是一个经典面试题,还是请朋友你先猜一下返回值吧。
public static int func3() {
try {
return 1;
} catch (Exception e){
return 2;
} finally {
return 3;
}
}
javap反编译字节码如下
public static int func3();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=0
0: iconst_1
1: istore_0
2: iconst_3
3: ireturn
4: astore_0
5: iconst_2
6: istore_1
7: iconst_3
8: ireturn
9: astore_2
10: iconst_3
11: ireturn
Exception table:
from to target type
0 2 4 Class java/lang/Exception
0 2 9 any
4 7 9 any
0~1: iconst_1 将常量1压入操作数栈顶,istore_0 将栈顶变量存储到局部变量表slot=0的位置。这就是代码return 1在finally块执行之前将需要返回的值0存为一个局部变量,同时这也印证了上一个例子对return的分析。
2~3: iconst_3 将常量3压入操作数栈顶,ireturn 返回栈顶的值并结束方法。这就是finally块中的代码return 3,因为这个return 3之后没有finally代码需要再执行了,所以此处的return 3就只有两个指令,同样,这也印证了上上个例子对return的分析。
如果没有发生异常,方法就到这里结束,所以返回值3。由此可知,当return后的finally中有return语句,则第一个return只是将返回值存为一个局部变量,而第二个的return会结束方法,而且第一个return存储的返回值也不会被使用。
这段逻辑用伪代码表示就是这样
int tmp = returnValue1; //第一个return 1在执行finally代码之前将返回值存为一个局部变量
finally...//finally里的其他代码
return 3;//finally里最后有一个return语句,这个return会返回3并结束方法,所以前一个return的返回值也没有用了
5.当finally遇到Throwable异常和return
源代码是这样子的,没有catch,try块中发生了异常,finally里有return语句。我们知道java中throw异常会结束方法,return也会结束方法,所以这个方法有返回值吗,又会抛出异常吗?
public static int func4() {
try {
throw new Exception();
} finally {
return 2;
}
}
同样通过字节码来分析
public static int func4();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=0
0: new #6 // class java/lang/Exception
3: dup
4: invokespecial #10 // Method java/lang/Exception."":()V
7: athrow
8: astore_0
9: iconst_2
10: ireturn
Exception table:
from to target type
0 9 8 any
0~4: new、dup、invokespecial #10这三个指令,熟悉字节码指令的人就会知道这是创建Exception对象,并将这个异常对象的引用压入操作数栈顶。
7: athrow 抛出操作数栈顶的异常对象。本来程序到这里就应该结束了,结合第二节,有finally的代码,编译器会为代码添加catch块,并将finally的代码复制到catch块中,以此确保finally一定会被执行。再仔细看方法的字节码,编译器为func4生成了一个异常表,所以该方法就相当于下面这样。
public static int func4() {
try {
throw new Exception();
} catch(Throwable t) {
return 2;
}
}
8: 指令7抛出了异常,被编译器生成的异常表捕获,并将异常对象放在操作数栈顶,astore_0 会将这个异常对象存到局部变量表slot=0的位置。
9~10: iconst_2、ireturn返回int 2,并结束方法。
由此可知,即使没有catch块,因为finally的存在,编译器也会为try添加一个catch块,捕获所有类型的异常(Throwable),然后将finally代码复制到catch块中,以此确保finally一定被执行。所以这个方法并不会抛出异常,会返回int型2。
本来以为finally很快就写完了,结果在写作过程中,发现各种好玩的case,所以花了我三四个小时,把各种情况都写出来了,希望对各位朋友有所帮助。
总结一下:
1.try...catch是通过异常表实现的。
2.java确保finally一定会被执行,是通过复制finally代码块到每个分支实现的。
3.通常return会被编译成2个指令,当return后还有finally,return就会被编译成4个指令。
4.return后还有finally,return会将返回值存储起来,finally中并不能改变返回值(当返回值是引用类型是另外一种情况,自行研究下)。
5.return后的finally里有return,则finally里的return会结束方法。
6.try中抛出异常后,finally里有return,则方法并不会抛出异常,会正常返回。
最后的最后,这一篇文章有点长,希望你耐心看完。留一道面试题,func2返回值A的属性aInt会是多少?这和上面的func2有什区别呢?
public static A func2() {
A a = new A();
a.aInt = 0;
try {
return a;
} catch (Exception e){
return a;
} finally {
a.aInt = a.aInt + 1;
}
}
public static class A{
public int aInt ;
}