由一道 Java finally 执行顺序的题引发的思考

前言

本文讲解过于繁琐,已重新修改至 关于 Java finally 执行顺序 -- 修改版

原题

首先来看看这道题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class test1 {
static String s = "A";

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

private static String test() {
try {
System.out.println("A");
return s = "A";
} finally {
System.out.println("B");
s = "B";
}
}
}

执行结果是 ABAB ,刚看到这个结果,我是比较诧异的,利用 IDEA 的单步调试也没弄明白,来看看我当时的思路:

  1. 首先输出 try 中的输出语句,输出 "A"
  2. 由于 try 下还有的 finally 语句,所以执行 finally 中的输出语句,输出 "B"
  3. "B" 赋值给变量 s
  4. 回到 try 中的 return 语句,再次将 "A" 赋值给 s,将 "A" 最为返回值,返回 main 方法中。(此时 s = "A")
  5. 回到 main 方法中的第一行输出语句,输出 "A",然后执行 main 方法中的第二条输出语句,输出 "A"

所以,结果不应该是 “ABAA” 么,因为 return 中已经将 “A” 赋值给 s 了,那么 s 的值不应该是 “A” 么,这时返回值就是 “A”,再怎么第二条输出语句也不能是 “B” 呀,可看着开发工具输出的结果,我也很无奈,我当时是这个表情。。。

但我相信开发工具是不会骗我的,一定是我自己的理解有问题,然后自己又写了几个类似的例子来测试,最后得出的结论是: finally 语句块在 return 语句之后,return 返回之前执行,可能这句话不是那么容易理解,来看下面的例子吧,看完我相信你应该能明白我这句话是什么意思了。

例一

证明:finally 语句块在 return 语句之后,return 返回之前执行。

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
public class FinallyTest1 {

public static void main(String[] args) {
System.out.println(test1());
}

public static int test1() {
int b = 20;

try {
System.out.println("try block");

return b += 80;
} catch (Exception e) {
System.out.println("catch block");
} finally {
System.out.println("finally block");

if (b > 25) {
System.out.println("b>25, b = " + b);
}
}

return b;
}

}

运行结果:

try block
finally block
b>25, b = 100
100

可以根据运行结果得知,在 finally 语句块执行之前,return b += 80; 中的 b + =80; 这部分已经运行,但是没有返回,然后等 finally 执行完以后才返回结果的。

例二

如果觉得这个 例 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
public class FinallyTest1 {

public static void main(String[] args) {

System.out.println(test11());
}

public static String test11() {
try {
System.out.println("try block");

return test12();
} finally {
System.out.println("finally block");
}
}

public static String test12() {
System.out.println("return statement");

return "after return";
}

}

运行结果:

try block
return statement
finally block
after return

这下总明白了吧,执行 finally 之前将 return 后的 test12() 方法先执行了,然后再执行的 finally 语句。

拓展问题一

前面两个例子证明了原题得出的结论,但是这个结论好像还不能足矣证明原题中的结果为什么是"ABAB",还有一个新问题:

如果,finally 中对 return 返回的变量进行了修改,那么 return 返回的是修改前的值还是修改后的值?

我们在 例 1finally 语句块中加上一句 b = 10;,那么结果又会是什么呢?

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
public class FinallyTest2 {

public static void main(String[] args) {

System.out.println(test1());
}

public static int test1() {
int b = 20;

try {
System.out.println("try block");

return b += 80;
} catch (Exception e) {

System.out.println("catch block");
} finally {

System.out.println("finally block");

if (b > 25) {
System.out.println("b>25, b = " + b);
}
b = 10;
}

return b;
}

}

运行结果:

try block
finally block
b>25, b = 100
100

可以看到 return 的值并没有被 finally 中的语句改变,其实如果这样得出结论:finally 中对 return 返回的变量进行了修改,并不会影响 try 中 return 的值,是不负责任的,因为我们只考虑了基本数据类型,如果是引用数据类型,还会是这种结果么?

扩展问题二

拓展问题 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
public class FinallyTest3 {

public static void main(String[] args) {

System.out.println(test1().getName());
}

public static Student test1() {
Student stu = new Student();

try {
stu.setName("Try");
return stu;
} catch (Exception e) {
stu.setName("Catch");
} finally {
stu.setName("Finally");
}

return stu;
}

}

class Student {
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

运行结果:

Finally

可以看到,对于基本数据类型和引用数据类型,结论是不同的,再次总结下结论吧:

  • 对于基本数据类型,finally 语句块中对 try 语句块的 return 的变量进行了修改,return 的则是修改前的值。
  • 对于引用数据类型,finally 语句块中对 try 语句块的 return 的变量进行了修改,return 的则是修改后的值。

看到这里,不知道你有没有想到这个结论与 Java 的方法调用时的所谓的 值传递引用传递 的结论有些类似。

既然得出了这个结论,那么这个结论是必然的情况么?一定正确么?别急,来看下一个例子。

拓展问题三

对于 拓展问题 2 我再做一个增强版的,其实就是在第 17 行之下添加一条语句 stu = null

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
public class FinallyTest4 {

public static void main(String[] args) {

System.out.println(test1().getName());
}

public static Student test1() {
Student stu = new Student();

try {
stu.setName("Try");
return stu;
} catch (Exception e) {
stu.setName("Catch");
} finally {
stu.setName("Finally");
stu = null;
}

return stu;
}

}

class Student {
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

运行结果:

Finally

这时,你可能都有骂人的冲动了,What ?咋回事,咋什么结论都不对呢,不是说好的引用数据类型会改变 return 的值么,这里都将 stu 修改为 null 了,怎么还能 return"Finally" 呢?

别激动,别激动,不卖关子了,我直接说了:

先说基本数据类型,由于基本数据类型是 值传递 ,所以在 try 里的 return 执行的时候,就先将变量的值隐性的作为最终返回的值。

同样的,对于引用数据类型,只是将该变量所指向的内存地址隐性的作为最终返回的值,所以即使将 stu = null,也无所谓了,因为早在执行 finally 之前,try 里的 return 就已经拿到了 stu 所指向的地址。

这里多说一句,其实 Java 里都是值传递,只不过基本数据类型的值是真正的值,而引用数据类型是地址值而已。(如果你看这句话更晕的话,就先跳过去这句话,反正本篇文章也不是为了解释这个的。)

更深的思考


问:如果 tryfinally 中都有 return 语句,那么到底会返回哪一个呢?

会返回 finally 中的 return


问:如果 try 里有 return 语句,整个 try-catch-finally 块之外也有一个 return 语句,那么哪个会执行,一定是这样么?

try 里没有发生异常的情况下,是 try 里的 return 会执行,但发生了异常,则反之。


问:如果 catch 中有 return 语句呢?当然只有在异常的情况下才有可能会执行,那么是在 finally 之前就返回吗?

当发生异常后,catch 中的 return 执行情况与未发生异常时 tryreturn 的执行情况完全一样。


最终总结

finally 块的语句在 try 或 catch 中的 return 语句执行之后返回之前执行,且 finally 里的修改语句可能影响也可能不影响 try 或 catch 中 return 已经确定的返回值,若 finally 里也有 return 语句则覆盖 try 或 catch 中的 return 语句直接返回。

本文参考