前言
因为面试中可能会被面试官问到,所以我自己还是先总结一波好了。
指令简单介绍
下面的说明只是为了理解,实际上并不是非常准确
- iconst_n:把n这个数字放入到栈中
- istore_n:把栈顶的数字放到局部变量表中的第n位,局部变量表是从0开始计数的。
- iload_n:将局部变量表的指定位置的相应类型变量加载到栈顶,相当于和istore_n是相反的操作。
不带return的情况
首先先来一道开胃菜:
1 | public static void main(String[] args) { |
该程序会输出什么?输出3,因为我们知道finally中的一定会被执行,不论有没有发生异常。如果我们把finally去掉,那么如果不发生异常,那么i就是1,如果发生了异常,就会进入到catch语句,也就是i会等于2。
我们来看看上面这段程序底层的指令长什么样吧,有助于更好的理解。
1 | 0 iconst_0 // 让0这个数字入操作数栈 |
上面的指令是不是让人有点晕?虽然可以很好的解释为什么i=3,但是这么做的逻辑让人看不懂,似乎是把finally的逻辑给加到try的最后了。
接下来再来看看由idea为我们反编译得到的文件,可能能够更加加深理解。
1 | public static void main(String[] args) { |
可以看到,反编译给优化成只有最后的i = 3;,其它的i=1和i=2甚至都不见了。
带return的情况
1 | public static int func() { |
如果我调用上面这个函数,那么执行之后,返回的会是什么?估计大部分人都能回答对:返回3。那么如果我在try语句中加入会发生异常的代码呢?还是返回3。好,确实,上面这段代码无论什么情况都会返回3(断电什么的极端情况请不要考虑)。那我稍微修改一下代码:
1 | public static int func() { |
那么,此时应该返回什么?接下来的分析就又要请编译后的文件出马了:
1 | 0 iconst_0 |
上面这段其实有点疑问的,为什么我这个函数明明只有一个变量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中的逻辑给你自动追加到try和catch中去了,导致了上面看似矛盾的发生。所以理解了这些,相信之后碰到任何这类问题都不怕了。
PS:实际中请尽量不要在finally语句中使用return,因为它会短路掉你在try和catch中的返回值。