关于在Java在异常中使用 return 关键字的情形我在网上搜索的一下,看了一会就感觉有点晕了,感觉大部分讲的不是很好。有一些博客甚至把这个当作一个概念或者特性来进行记忆了,在什么什么情况下该怎么怎么样,完全是靠死记硬背来区分的。

我觉得死记硬背是不对的,所以我就尝试使用反编译字节码来观察指令的方式来观察这个现象,发现问题其实很简单。比如我们先举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Test {

public static void main(String[] args) {
int j = test();
System.out.println(j);
}

private static int test() {
int i;
try {
i = 100;
return i;
} catch (Exception e) {
e.printStackTrace();
i = 200;
return i;
} finally {
i = 300;
}
}

}

请问这个例子中最终打印的 test() 方法的返回值 j 的值是多少呢?

想要知道结果,其实并不需要复杂的人肉去分析整个流程,只需要简单的反编译一下字节码就足够了。

我们先对源码进行编译:

javac Test.java

编译得到了 Test.class 文件,我们可以先执行一下看看

java Test

执行的结果是 100,那么这个结果是怎么得到的呢?我们尝试对字节码文件进行反编译

javap -c -private Test

反编译后我们得到如下结果注1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: invokestatic #2 // Method test:()I
3: istore_1
4: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
7: iload_1
8: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
11: return

private static int test();
Code:
0: bipush 100
2: istore_0
3: iload_0
4: istore_1
5: sipush 300
8: istore_0
9: iload_1
10: ireturn
11: astore_1
12: aload_1
13: invokevirtual #6 // Method java/lang/Exception.printStackTrace:()V
16: sipush 200
19: istore_0
20: iload_0
21: istore_2
22: sipush 300
25: istore_0
26: iload_2
27: ireturn
28: astore_3
29: sipush 300
32: istore_0
33: aload_3
34: athrow
Exception table:
from to target type
0 5 11 Class java/lang/Exception
0 5 28 any
11 22 28 any
}

反编译得到的信息比较多,很多部分是我们不关心的,我们重点关心 test() 方法内部的执行过程,即第20~27行:

0: bipush        100
2: istore_0
3: iload_0
4: istore_1
5: sipush        300
8: istore_0
9: iload_1
10: ireturn

我们接下来了解下这几句指令做了什么事情:

  1. 向操作数栈中压入立即数 100
  2. 把操作数栈的栈顶元素弹出并且赋值给局部变量表中的第0个变量
  3. 把局部变量表中第0个元素压入栈顶
  4. 把栈顶元素弹出并赋值给局部变量表中的第1个变量
  5. 向操作数栈中压入立即数300
  6. 把栈顶元素赋值给局部变量表的第0个变量
  7. 把局部变量表中的第1个元素压入栈顶
  8. 从当前方法返回栈顶的int元素

以上指令执行的过程中局部变量和操作数栈的状态如下图所示:

我们重点发现在第4条指令中我们把栈顶元素(即 100)存入了第1个局部变量中,之后在第7条指令中又把第1个局部变量的值压入栈顶,之后立即执行 ireturn 指令把栈顶元素从当前方法返回。从分析中我们可以很清晰的得出返回值就是100的结论,有了指令的描述我们就不再需要看着源代码来猜测方法的运行结果了。

这里的例子是 return 关键字在正常的执行流程中生效的情况,对于 return 在异常或者finally中生效的情况,我们根本无需猜测或者查阅资料,只需要对源代码生成的字节码进行反编译即可。另外两种情况就留给读者自己去练习了,操作方式和和上面的一模一样,只需要仔细观察在执行 ireturn 的时候栈顶元素的值就可以了。

注1:如果对Java指令不熟悉可以参考《深入理解Java虚拟机:JVM高级特性与最佳实践》的附录B,如果没有书的话可以参考此链接:https://segmentfault.com/a/1190000008722128