Java中关于try、catch、finally中的细节分析

前言

阿里巴巴开发手册中有这么一条:【强制】不要在 finally 块中使用 return , 在开发过程中发现部分同学对这条规则理解不是很透彻,本文将就 try 、catch、finally 的一些问题,分析一下 try 、catch、finally 的处理流程。

首先看一个例子:

1

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

public static String test() {
String t = "";

try {
t = "try";
return t;
} catch (Exception e) {
t = "catch";
return t;
} finally {
t = "finally";
}
}

public static void main(String[] args) {
System.out.print(TryCatchFinally.test()); // print try
}
}

首先程序执行try语句块,把变量 t 赋值为 try,由于没有发现异常,接下来执行 finally 语句块,把变量 t 赋值为”finally”,然后return t,则 t 的值是 “finally”,最后 t 的值就是 “finally”,程序结果应该显示 “finally”,但是实际结果为 “try” 。

为什么会这样,我们不妨先看看这段代码编译出来的class对应的字节码,看虚拟机内部是如何执行的。

首先将类编译成 class

1
javac -g:vars TryCatchFinally.java

显示目标 class 文件的字节码信息

1
javap -verbose TryCatchFinally 

编译出来的字节码部分,我们只需关注 test 方法,其它先忽略掉。关于jvm虚拟机字节码指令意思,可查阅 Java 虚拟机字节码指令表

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
public static java.lang.String test();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=0
0: ldc #2 // String
2: astore_0
3: ldc #3 // String try
5: astore_0
6: aload_0
7: astore_1
8: ldc #4 // String finally
10: astore_0
11: aload_1
12: areturn
13: astore_1
14: ldc #6 // String catch
16: astore_0
17: aload_0
18: astore_2
19: ldc #4 // String finally
21: astore_0
22: aload_2
23: areturn
24: astore_3
25: ldc #4 // String finally
27: astore_0
28: aload_3
29: athrow
Exception table:
from to target type
3 8 13 Class java/lang/Exception
3 8 24 any
13 19 24 any
LocalVariableTable:
Start Length Slot Name Signature
14 10 1 e Ljava/lang/Exception;
3 27 0 t Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 13
locals = [ class java/lang/String ]
stack = [ class java/lang/Exception ]
frame_type = 74 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]

首先看LocalVariableTable信息,这里面定义了两个变量 一个是 t String类型,一个是 e Exception 类型

接下来看Code部分

  • 第[0-2]行,给第一个本地变量赋值””,也就是String t=””;
  • 第[3-6]行,也就是执行try语句块 赋值语句 ,也就是 t = “try”;
  • 第7行,重点是第7行,把第s对应的值”try”付给第二个本地变量,但是这里面第二个本地变量并没有定义,这个比较奇怪
  • 第[8-10] 行,对第一个变量进行赋值操作,也就是t=”finally”
  • 第[11-12]行,把第二个变量对应的值返回

通过字节码,我们发现,在 try 语句的 return 块中,return 返回的引用变量( t 是引用类型)并不是try语句外定义的引用变量t,而是系统重新定义了一个局部引用 t ’,这个引用指向了引用 t 对应的值,也就是 “try” ,即使在 finally 语句中把引用 t 指向了值 “finally” ,因为 return 的返回引用已经不是 t ,所以引用 t 的对应的值和 try 语句中的返回值无关了。

下面再看一个例子:

例2

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

public static String test() {
String t = "";

try {
t = "try";
return t;
} catch (Exception e) {
t = "catch";
return t;
} finally {
t = "finally";
return t;
}
}

public static void main(String[] args) {
System.out.print(TryCatchFinally.test()); // print finally
}
}

这里稍微修改了 第一段代码,只是在 finally 语句块里面加入了 一个 return t 的表达式。

按照第一段代码的解释,先进行try{}语句,然后在 return 之前把当前的t的值 try 保存到一个变量 t’,然后执行 finally 语句块,修改了变量 t 的值,在返回变量 t。

这里面有两个return语句,但是程序到底返回的是 “try” 还是 “finally” 。接下来我们还是看字节码信息

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
public static java.lang.String test();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=0
0: ldc #2 // String
2: astore_0
3: ldc #3 // String try
5: astore_0
6: aload_0
7: astore_1
8: ldc #4 // String finally
10: astore_0
11: aload_0
12: areturn
13: astore_1
14: ldc #6 // String catch
16: astore_0
17: aload_0
18: astore_2
19: ldc #4 // String finally
21: astore_0
22: aload_0
23: areturn
24: astore_3
25: ldc #4 // String finally
27: astore_0
28: aload_0
29: areturn
Exception table:
from to target type
3 8 13 Class java/lang/Exception
3 8 24 any
13 19 24 any
LocalVariableTable:
Start Length Slot Name Signature
14 10 1 e Ljava/lang/Exception;
3 27 0 t Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 13
locals = [ class java/lang/String ]
stack = [ class java/lang/Exception ]
frame_type = 74 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]

继续看code属性

前10行第一段代码逻辑一样,重点看11行

11行执行 finally 里面的赋值语句,把变量 t 赋值为 “finally”,然后返回t对应的值

我们发现try语句中的 return 语句给忽略。可能 jvm 认为一个方法里面有两个 return 语句并没有太大的意义,所以 try 中的 return 语句给忽略了,直接起作用的是 finally 中的 return 语句,所以这次返回的是 “finally”。

再看看复杂一点的例子:

例3

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

public static String test() {
String t = "";

try {
t = "try";
Integer.parseInt(null);
return t;
} catch (Exception e) {
t = "catch";
return t;
} finally {
t = "finally";
}
}

public static void main(String[] args) {
System.out.print(TryCatchFinally.test()); // print catch
}
}

这里面 try 语句里面会抛出 java.lang.NumberFormatException,所以程序会先执行 catch 语句中的逻辑,t 赋值为 catch ,在执行return 之前,会把返回值保存到一个临时变量里面 t ‘,执行 finally 的逻辑,t 赋值为 “finally”,但是会返回 t’,所以变量 t 的值和返回值已经没有关系了,返回的是 “catch”

例4

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

public static String test() {
String t = "";

try {
t = "try";
Integer.parseInt(null);
return t;
} catch (Exception e) {
t = "catch";
return t;
} finally {
t = "finally";
return t;
}
}

public static void main(String[] args) {
System.out.print(TryCatchFinally.test()); // print finally
}
}

这个和例 2 有点类似,由于 try 语句里面抛出异常,程序转入 catch 语句块,catch 语句在执行 return 语句之前执行 finally ,而 finally语句有 return ,则直接执行 finally 的语句值,返回 “finally”.

例5

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

public static String test() {
String t = "";

try {
t = "try";
Integer.parseInt(null);
return t;
} catch (Exception e) {
t = "catch";
Integer.parseInt(null);
return t;
} finally {
t = "finally";
}
}

public static void main(String[] args) {
System.out.print(TryCatchFinally.test());// throw java.lang.NumberFormatException
}
}

这个例子在catch语句块添加了Integer.parser(null)语句,强制抛出了一个异常。然后 finally 语句块里面没有 return 语句。

继续分析一下,由于 try 语句抛出异常,程序进入 catch 语句块,catch 语句块又抛出一个异常,说明 catch 语句要退出,则执行 finally语句块,对 t 进行赋值。然后 catch 语句块里面抛出异常。

结果是抛出java.lang.NumberFormatException异常

例6

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

public static String test() {
String t = "";

try {
t = "try";
Integer.parseInt(null);
return t;
} catch (Exception e) {
t = "catch";
Integer.parseInt(null);
return t;
} finally {
t = "finally";
return t;
}
}

public static void main(String[] args) {
System.out.print(TryCatchFinally.test()); // print finally
}
}

这个例子和上面例子中唯一不同的是,这个例子里面 finally 语句里面有 return 语句块。try catch 中运行的逻辑和上面例子一样,当catch 语句块里面抛出异常之后,进入 finally 语句快,然后返回 t 。则程序忽略 catch 语句块里面抛出的异常信息,直接返回 t 对应的值 也就是 “finally”。方法不会抛出异常

例7

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

public static String test() {
String t = "";

try {
t = "try";
Integer.parseInt(null);
return t;
} catch (NullPointerException e) {
t = "catch";
return t;
} finally {
t = "finally";
}
}

public static void main(String[] args) {
System.out.print(TryCatchFinally.test()); // throw java.lang.NumberFormatException
}
}

这个例子里面 catch 语句里面 catch 的是 NPE 异常,而不是 java.lang.NumberFormatException异常,所以不会进入 catch 语句块,直接进入 finally 语句块,finally 对 t 赋值之后,由 try 语句抛出java.lang.NumberFormatException异常。

例8

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

public static String test() {
String t = "";

try {
t = "try";
Integer.parseInt(null);
return t;
} catch (NullPointerException e) {
t = "catch";
return t;
} finally {
t = "finally";
return t;
}
}

public static void main(String[] args) {
System.out.print(TryCatchFinally.test()); // print finally
}
}

和上面的例子中 try catch 的逻辑相同,try 语句执行完成执行 finally 语句,finally 对 t 赋值 并且返回 ,最后程序结果返回 “finally”

例9

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

public static String test() {
String t = "";

try {
t = "try";
return t;
} catch (Exception e) {
t = "catch";
return t;
} finally {
t = "finally";
String.valueOf(null);
return t;
}
}

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

这个例子中,对 finally 语句中添加了String.valueOf(null), 强制抛出 NPE 异常。首先程序执行 try 语句,再执行finally语句块,finally 语句抛出 NPE 异常,整个结果返回NPE异常。

总结

  1. try、catch、finally 语句中,在如果 try 语句有 return 语句,则返回当前 try 中变量指向的值,此后 变量 指向的改变都不会影响 try 中 return 的返回
  2. 如果 finally 块中有 return 语句,则 try 或 catch 中的返回语句会被忽略
  3. 如果 finally 块中抛出异常,则整个 try、catch、finally块中抛出异常
  4. 【强制】不要在 finally 块中使用 return

The END.


Java中关于try、catch、finally中的细节分析
https://www.weypage.com/2021/10/06/java/日常/Java中关于try、catch、finally中的细节分析/
作者
weylan
发布于
2021年10月6日
许可协议