今天我在《深入理解Java虚拟机》这本书中看到一个特别有意思的例子,作者在书中举了一个try-catch-finally
的例子,然后给出了答案。我看着答案,感觉自己想的不一样,当时我以为答案错了,然后自己在编译器上照着写了一下那个例子,发现最终的答案跟书上是一模一样的。瞬间我就懵逼了,难道我学的假Java?(其实归根结底还是自己菜😂)。
1. 提出问题
当时书中的例子是这样的:
public class Demo {
public static void main(String[] args) {
Demo demo = new Demo();
System.out.println("val = " + demo.inc());
}
public int inc() {
int x;
try {
x = 1;
return x;
} catch (Exception e) {
x = 2;
return x;
} finally {
x = 3;
}
}
}
重点在inc
方法里面,main
方法只是用来测试的。当时书中说,如果在try语句块没有抛出异常,inc
方法最终会返回1
。大家看到这里似乎没有发现问题。
我来解释一下我的理解,地球人都知道,在try-catch-finally
语句中,如果try语句块中会return,那么在return之前会执行finally语句块里面的代码。此时,大家会不会恍然大悟,对啊,在return之前会执行finally语句块里面的代码。在上面的例子中,我们在finally语句块中将x赋值为3,然后再return x
,为什么x还是为1呢?
从血淋淋的现实中,我们可以知道,我们的Java还没有学到家。那原因是什么呢?接下来我将从两个方面解释这个现象。
2. 从class文件来看
上面的Java文件经过javac工具编译之后,会生成一个对应的class文件。我们用编译器(我使用的是idea)打开看看:
public class Demo {
public Demo() {
}
public static void main(String[] args) {
Demo demo = new Demo();
System.out.println("val = " + demo.inc());
}
public int inc() {
byte var3;
try {
byte x;
try {
x = 1;
byte var2 = x;
return var2;
} catch (Exception var7) {
x = 2;
var3 = x;
}
} finally {
boolean var9 = true;
}
return var3;
}
}
楼主使用的jdk版本是1.8.0_201
,可能版本不同编译出来的是不一样的。
从class文件,我们可以直观的知道,虽然我们在finally语句块中给x赋值为3,但是编译之后,finally语句里面根本没有对x进行赋值。实际上的是,在try语句块中,return之前已经将x赋值给一个名为var2
的变量,然后最后返回也是var2变量。所以,finally语句块里面对x的修改根本没有起作用。
从class文件里面,我们可以非常直观的知道答案。接下来,我们将从字节码命令来分析这个问题。
3. 从字节码命令来看
我们使用javap工具来打印Demo 的class文件的具体信息,通过如下的命令即可:
javap -verbose class文件的路径
打印之后,我们得到inc
方法的字节码命令,如下(jdk版本不同,信息可能不一样):
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=5, args_size=1
0: iconst_1
1: istore_1
2: iload_1
3: istore_2
4: iconst_3
5: istore_1
6: iload_2
7: ireturn
8: astore_2
9: iconst_2
10: istore_1
11: iload_1
12: istore_3
13: iconst_3
14: istore_1
15: iload_3
16: ireturn
17: astore 4
19: iconst_3
20: istore_1
21: aload 4
23: athrow
在分析之前,我先说一下:
- 这里只是展示inc方法的部分信息,并没有展示全部。
- 为了简单起见,这里只分析0 ~ 7行命令。
我们先来看一下0 ~ 1两行指令:
0: iconst_1 // 将int型变量1推至栈顶。
1: istore_1 // 将栈顶的变量赋值给第二个本地变量,也就是x = 1。
然后是2~3两行指令:
2: iload_1 // 将第二个本地变量推至栈顶,这里表示的意思是将1放在栈顶
3: istore_2 // 将栈顶的变量值赋值给第三个本地变量(这里我们假设第三个本地变量是`returnValue`),表示的意思相当于将栈顶的x的值用另一个变量来保存。
然后是4~5两行指令:
4: iconst_3 // 将int类型变量3推至栈顶
5: istore_1 // 将栈顶变量赋值给第二个本地变量,也就是x = 3。这部分的指令就表示finally语句块的代码。
然后是6~7两行指令:
6: iload_2 // 将第三个本地变量推至栈顶,也就是将`returnValue`放在栈顶。
7: ireturn // 返回int值。
最后的ireturn
指令表示获取栈顶变量的值。
4. 总结
到这里,我们就清楚了上面的问题,为什么在finally语句块给x赋值为3,最后返回还是1,我来简单的总结一下。
在调用finally语句块代码之前,jdk会将需要return的值保存在另一个变量(
returnValue
)中,最后返回也是returnValue
变量。
网友评论