Java中finally和return的返回值

前言

因为面试中可能会被面试官问到,所以我自己还是先总结一波好了。

指令简单介绍

下面的说明只是为了理解,实际上并不是非常准确

  • iconst_n:把n这个数字放入到栈中
  • istore_n:把栈顶的数字放到局部变量表中的第n位,局部变量表是从0开始计数的。
  • iload_n:将局部变量表的指定位置的相应类型变量加载到栈顶,相当于和istore_n是相反的操作。

不带return的情况

首先先来一道开胃菜:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
int i = 0;
try {
i = 1;
} catch (Exception e) {
i = 2;
e.printStackTrace();
} finally {
i = 3;
}
System.out.println(i);
}

该程序会输出什么?输出3,因为我们知道finally中的一定会被执行,不论有没有发生异常。如果我们把finally去掉,那么如果不发生异常,那么i就是1,如果发生了异常,就会进入到catch语句,也就是i会等于2。

我们来看看上面这段程序底层的指令长什么样吧,有助于更好的理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 0 iconst_0								// 让0这个数字入操作数栈
1 istore_1 // 从操作数栈中弹出栈顶元素(也就是0),并且放入到局部变量表的第一个位置,即i=0
2 iconst_1 // 让1这个数字入操作数栈
3 istore_1 // 从操作数栈中弹出栈顶元素(也就是1),并且放入到局部变量表的第一个位置,即i=1,此时已经进入try语句了
4 iconst_3 // 让3这个数字入操作数栈
5 istore_1 // 从操作数栈中弹出栈顶元素(也就是3),并且放入到局部变量表的第一个位置,即i=3,此时已经进入finally语句了
6 goto 26 (+20) // 跳转到输出
9 astore_2 // 下面是异常处理阶段,请暂时忽略
10 iconst_2
11 istore_1
12 aload_2
13 invokevirtual #3 <java/lang/Exception.printStackTrace>
16 iconst_3
17 istore_1
18 goto 26 (+8)
21 astore_3
22 iconst_3
23 istore_1
24 aload_3
25 athrow
26 getstatic #4 <java/lang/System.out>
29 iload_1
30 invokevirtual #5 <java/io/PrintStream.println>
33 return

上面的指令是不是让人有点晕?虽然可以很好的解释为什么i=3,但是这么做的逻辑让人看不懂,似乎是把finally的逻辑给加到try的最后了。

接下来再来看看由idea为我们反编译得到的文件,可能能够更加加深理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
boolean i = false;

byte i;
try {
i = true;
} catch (Exception var6) {
i = true;
var6.printStackTrace();
} finally {
i = 3;
}

System.out.println(i);
}

可以看到,反编译给优化成只有最后的i = 3;,其它的i=1i=2甚至都不见了。

带return的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
public static int func() {
int i = 0;
try {
i = 1;
return i;
} catch (Exception e) {
i = 2;
return i;
} finally {
i = 3;
return i;
}
}

如果我调用上面这个函数,那么执行之后,返回的会是什么?估计大部分人都能回答对:返回3。那么如果我在try语句中加入会发生异常的代码呢?还是返回3。好,确实,上面这段代码无论什么情况都会返回3(断电什么的极端情况请不要考虑)。那我稍微修改一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static int func() {
int i = 0;
try {
i = 1;
return i;
} catch (Exception e) {
i = 2;
return i;
} finally {
i = 3;
// 我把这里的return删掉了
}
}

那么,此时应该返回什么?接下来的分析就又要请编译后的文件出马了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 0 iconst_0
1 istore_0 // i = 0
2 iconst_1
3 istore_0 // try块中的 i = 1
4 iload_0 // 把 i 放入到栈顶
5 istore_1 // 把 i 放入到局部变量表的第二个变量中。 你肯定好奇,这段函数明明只有一个变量i,哪里来的第二个变量?
6 iconst_3
7 istore_0 // i = 3
8 iload_1 // 把第二个变量放到栈顶中
9 ireturn // 返回!
10 astore_1 // 从这里开始是异常处理阶段,把第一个ref变量入栈
11 iconst_2 // 把2这个数字放入栈顶
12 istore_0 // 把2放到局部变量表的第0位(也就是i = 2)
13 iload_0 // 把 i 放入栈顶
14 istore_2 // 把2赋值给局部变量表中第三个元素
15 iconst_3 // 把3放入栈顶
16 istore_0 // 把3赋值给局部变量表中的第一个元素,也就是 i = 3
17 iload_2 // 把局部变量表中第三个元素(目前的值是2)放入栈顶
18 ireturn // 返回,异常处理结束
19 astore_3
20 iconst_3
21 istore_0
22 aload_3
23 athrow

上面这段其实有点疑问的,为什么我这个函数明明只有一个变量i,但是却出现了istore_1(把栈顶元素放到局部变量表的第二个元素中)?其实稍微想一想,其实这个所谓的第二个元素,就是需要被返回的值。

而且从上面的指令中我们可以看到,也确实,finally确实是被执行到了,也就是此时i确实是等于3了,但是由于之前在try语句块中,我先把i放到了另外一个地方(如果不理解,你不妨可以理解成我额外有一个returnValue的变量,我把i已经赋值给它了),最后返回的时候,会让这个值最后再加载到栈顶并且返回。

所以上面这段代码就很好解释了:

  • 如果正常执行,那么只会从上面的0开始执行到9那句的ireturn返回,而此时其实已经包含了finally语句块了。
  • 如果出现了异常,且该异常能够被捕捉,那么就会到10开始,然后执行到18的ireturn返回,这段中其实也已经把finally的逻辑加入进去了。
  • 如果出现了异常,且该异常无法捕捉,那么就执行athrow这条命令,抛出异常。

总结

其实异常处理机制和return结合在一起的情况根本就不难,难是因为java给的try...catch...finally这个语法糖。

在上java课的时候,老师说的是java中的异常处理机制中,finally是一定会被执行的,而同时也说了,程序执行到return就返回

上面这两句话有问题吗?没有,程序确实是这么做的。那为什么这两句话看上去有矛盾呢?这就是java语法糖的“副作用”——它自动帮你把finally中的语句块给擅自移动到它应该去的位置了,相当于编译器没跟你打招呼就把finally中的逻辑给你自动追加到trycatch中去了,导致了上面看似矛盾的发生。所以理解了这些,相信之后碰到任何这类问题都不怕了。

PS:实际中请尽量不要在finally语句中使用return,因为它会短路掉你在trycatch中的返回值。