.
.
.
.
.
今天有同事和我探讨在群里看到的一道有趣的题目,在探讨的过程中让我搞清楚了一些曾经模糊的概念,特此记录下来。
题目给出如下代码,问运行后打印的结果是什么。
1 public static void main(String []args) { 2 System.out.println(fun()); 3 } 4 public static int fun () { 5 int x = 0; 6 try { 7 x = 1; 8 } finally { 9 ++x; 10 } 11 try { 12 return x; 13 } finally { 14 ++x; 15 } 16 }
尝试运行,结果如下:(输出 2)
1 >$ javac -g No1.java 2 >$ javac No1 3 2 4 >$
为何输出是 2 而不是 3 呢,这个可能让很多小伙伴有所疑惑,我们通过 javap 指令查看字节码来解释这个疑问。
首先来看一些前置知识——java 字节码指令的 iconst_n、iload_n、istore_n 和 iinc。
指令 | 意义 |
iconst_n | 表示将整型常量 n 推入栈顶,n 的范围是 -1 ≤ n ≤ 5。 |
iload_n |
表示将局部变量表中第 n 个槽的整型变量加载到操作数栈顶。 |
istore_n |
表示将操作数栈顶的整型值弹出,并存储到局部变量表的第 n 个槽中。 |
iinc a b | 表示将局部变量表第 a 个槽中的值增加 b,并将结果存回 a 槽。 |
好,有了上面四个指令的基础就够了,接下来我们看一下上面代码的字节码。
>$ javap -c -l No1 public static int fun(); Code: 0: iconst_0 // 常量 0 入栈顶 1: istore_0 // 将栈顶的 0 弹出并存入局部变量表的第 0 个槽中 2: iconst_1 // 常量 1 入栈顶 3: istore_0 // 弹出栈顶的 1 并存入局部变量表的第 0 个槽中,覆盖原值 4: iinc 0, 1 // 将局部变量表第 0 个槽中的值加 1,并将结果存回局部变量表第 0 个槽中,覆盖原值 7: goto 16 // 跳转到 16 10: astore_1 11: iinc 0, 1 14: aload_1 15: athrow // 下面两行是重点,将局部变量表第 0 个槽中的值拷贝了一个副本到局部变量表第 1 个槽中 16: iload_0 // 将局部变量表第 0 个槽中的值加载到栈顶 17: istore_1 // 弹出栈顶的值并存储到局部变量表第 1 个槽中 18: iinc 0, 1 // 将局部变量表第 0 个槽中的值加 1,并将结果存回局部变量表第 0 个槽中,覆盖原值 21: iload_1 // 重点:将局部变量表中第 1 个槽中的值加载到栈顶(并没有加载第 0 个槽中的值) 22: ireturn // 返回栈顶的值 23: astore_2 24: iinc 0, 1 27: aload_2 28: athrow Exception table: from to target type 2 4 10 any 16 18 23 any
上面的文字太抽象,可以对照着 图1 的内容来理解。
图1 执行过程内存图例
在 图1 中,红色的字表示字节码的行号,黑色的字表示执行此行字节码之后,对应的内存中的值的变化。
不难看出,在字节码第 16、17 行,将 x 的值从局部变量表的第 0 个槽中拷贝了一个副本,保存在局部变量表的第 1 个槽中,而最后执行 ireturn 指令之前,将此副本加载到了栈顶,因此返回的值是在 finally 运算之前就确定下来了的,此后 finally 中再次对 x 的运算都只是在局部变量表的第 0 个槽中做的,所以并不会影响到 ireturn 指令返回的值。
总结:
经过 LZ 几番测试发现,无论在 try 中 return 的变量是否参与了后面在 finally 中的计算,都会被拷贝一个副本出来。
而 return 没有在 try 块中时,被 return 的变量则不会被拷贝副本。
由此可见,当 try 遇到 return 时,变量被“特殊照顾”了一下。
*注意,以上测试仅使用了 int 类型(基本数据类型),没有测试 return 引用类型的情况。