首页 - 信息 - 通过指令代码确定Java代码的执行顺序(++问题和return和finally问题)

通过指令代码确定Java代码的执行顺序(++问题和return和finally问题)

2023-10-06 20:39
-->

问题

我在书中遇到如下代码《深入理解Java虚拟机》:

公共 int 方法() {
int i;
尝试一下{
我 = 1;
return我;
} catch (异常 e) {
我 = 2;
return我;
}终于{
我= 3;
}
}

搜索了关于return andfinally的问题后,只是看到finally会被执行,这导致我误认为我只是把finally的执行顺序放在了return的语句之前,所以判断了这段代码的执行。结果应该是3,但实际结果是1,研究了一下,发现一开始确实很困惑,所以记录下来。

工具

我们都知道class文件中的内容是JVM可以理解的字节码。 JVM也是根据类的字节码来执行程序代码的,因此类文件中包含了程序代码的最终执行顺序。

我们可以通过官方的javap -c加上cla​​ss文件的路径得到每个方法对应的指令代码。

例如:javap -c Test.class

引用示例

由于我们打算使用JVM指令代码来解决这个问题,所以我一开始就简单的解释一下。对于以下方法:

public int method1() {
int i = 1;
return我;
}

该方法对应的命令代码为:

public int method1();
代码:
0:iconst_1
1:istore_1
2:iload_1
3:伊return

每条指令对应一个操作。以上指令代码的意思是:

  1. 将int值0压入栈顶
  2. 将栈顶的int类型元素存放到第二个空间
  3. 将第二个空间的int类型元素压入栈顶
  4. 返回栈顶int类型元素并退出该方法

由此可见,通过指令代码,我们可以直观地看到程序代码的执行顺序,是解决任何执行顺序问题的有力工具。

如果你还觉得有点不清楚,那么我们可以看看i++++i的问题。对于以下代码:

//return1
public int method2() {
int i = 1;
returni++;
} //return2
public int method3() {
int i = 1;
return++i;
}

他们的命令代码是:

public int method2();
代码:
0:iconst_1
1:istore_1
2:iload_1
3: iinc 1, 1
6:return public int method3();
代码:
0:iconst_1
1:istore_1
2: iinc 1, 1
5:iload_1
6:伊return

显然,这两个指令代码最大的区别在于iinc 1,1指令的位置不同,如果删除这条指令,它将与method1一致,对应源码,这条指令是符号++的影响。

而这个按键的功能iinc 1,1指令即使你完全不懂也能猜到。就是将第二个空间的int数据+1,然后放回第二个空间。空间

把这个意思放到命令码里再写一遍。以方法2为例:

  1. 将int值0压入栈顶
  2. 将栈顶的int类型元素存放到第二个空间
  3. 将第二个空间的int类型元素(1)压入栈顶
  4. +1第二个空间的int数据,然后放回第二个空间
  5. 返回栈顶int类型元素并退出该方法

需要注意的是,第三步是压入1而不是整个空间到栈顶,所以第四步在栈顶加1后并不改变栈顶。第二个空格中的数据1。 value,所以返回值为1。相比之下,method2是:

  1. 将int值0压入栈顶
  2. 将栈顶的int类型元素存放到第二个空间
  3. +1第二个空间的int数据,然后放回第二个空间
  4. 将第二个空间中的int类型元素(2)压入栈顶
  5. 返回栈顶int类型元素并退出该方法

所以,返回值为2。

解决方案

现在我们可以看看原来的方法方法。再次复制代码:

公共 int 方法() {
int i;
尝试一下{
我 = 1;
return我;
} catch (异常 e) {
我 = 2;
return我;
}终于{
我= 3;
}
}

对应命令码:

public int method();
代码:
0:iconst_1
1:istore_1
2:iload_1
3:istore 4
5:iconst_3
6:istore_1
7:iload 4
9:伊return
10:astore_2
11:iconst_2
12:istore_1
13:iload_1
14:istore 4
16:iconst_3
17:istore_1
18:iload 4
20:伊return
21:astore_3
22:iconst_3
23:istore_1
24:aload_3
25:投掷
异常表:
从 到 目标类型
0 5 10 类 java/lang/Exception
0 5 21 任意
10 16 21 任意

这个命令代码的不同之处在于最后有一个异常表。我们暂时不关心它。我们先看第一个ireturn命令的命令代码,也就是代码中的第9行。脚本代码:

0:iconst_1
1:istore_1
2:iload_1
3:istore 4
5:iconst_3
6:istore_1
7:iload 4
9:伊return

该命令码是无异常情况下程序执行的命令码。已经包含finally语句块的命令代码:

  1. 将int值1压入栈顶
  2. 将栈顶的int类型元素存放到第二个空间
  3. 将第二个空间的int类型元素(1)压入栈顶
  4. 将栈顶的int类型元素存放到第五个空间
  5. 将int值3压入栈顶
  6. 将栈顶的int类型元素存放到第二个空间(3)
  7. 的第五个空间的int类型元素(1)推入栈顶
  8. 返回栈顶int类型元素并退出该方法

可以看到该方法在第五个空间返回1,而不是在第二个空间返回3,与运行结果一致。

其中,重点是第四步和第七步。可见,当Java程序在执行过程中遇到return语句时,会首先保存方法的返回值。如果有finally语句块,那么会先执行finally语句块,最后取出返回值。返回

另外,如果return后面跟的是表达式或者方法,会先计算最终的返回值,然后再执行finally语句块,这个可以自行验证。

当然,如果保存的返回值是引用类型变量,那么在finally代码块中修改就会改变变量本身的属性,从而改变返回值的属性。毕竟finally的代码确实执行了。 。

比如返回一个List,在finally中添加或删除该List,返回的List的内容自然会发生变化。

额外

关于剩下的指令代码,涉及到的知识较多,所以我就根据自己的理解简单说一下。

该指令代码末尾有异常表。其含义可以简单解释为:在[,)的区间内,如果出现type类型的异常,则跳转到目标 执行。

因为异常表的存在,当异常发生时,程序可以根据产生的异常跳转到正确的位置执行下一段代码。

[10,20]是catch代码块对应的指令代码,但是捕获到的异常会被存储,也就是源码中的Exception e。 [21,25]会保存try语句块中抛出的未被catch捕获的异常,然后执行finally代码,最后抛出异常结束方法。

这三段指令代码都包含了finally指令代码,保证了源码中的finally代码一定会被执行。

结论

Java程序在执行过程中遇到return语句时,会首先保存方法的返回值。如果有finally语句块,那么会先执行finally语句块,最后取出返回值返回。另外,如果return后面跟的是表达式或者方法,那么会先计算最终的返回值,然后再执行finally语句块。

笔记内容只是我自己的想法和写作。如有疑问,请指出,谢谢!

-->