前言 阿里巴巴开发手册中有这么一条:【强制】不要在 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()); } }
首先程序执行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 2 : astore_0 3 : ldc #3 5 : astore_0 6 : aload_0 7 : astore_1 8 : ldc #4 10 : astore_0 11 : aload_1 12 : areturn 13 : astore_1 14 : ldc #6 16 : astore_0 17 : aload_0 18 : astore_2 19 : ldc #4 21 : astore_0 22 : aload_2 23 : areturn 24 : astore_3 25 : ldc #4 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 offset_delta = 13 locals = [ class java /lang/String ] stack = [ class java /lang/Exception ] frame_type = 74 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()); } }
这里稍微修改了 第一段代码,只是在 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 2 : astore_0 3 : ldc #3 5 : astore_0 6 : aload_0 7 : astore_1 8 : ldc #4 10 : astore_0 11 : aload_0 12 : areturn 13 : astore_1 14 : ldc #6 16 : astore_0 17 : aload_0 18 : astore_2 19 : ldc #4 21 : astore_0 22 : aload_0 23 : areturn 24 : astore_3 25 : ldc #4 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 offset_delta = 13 locals = [ class java /lang/String ] stack = [ class java /lang/Exception ] frame_type = 74 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()); } }
这里面 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()); } }
这个和例 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()); } }
这个例子在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()); } }
这个例子和上面例子中唯一不同的是,这个例子里面 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()); } }
这个例子里面 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()); } }
和上面的例子中 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异常。
总结
try、catch、finally 语句中,在如果 try 语句有 return 语句,则返回当前 try 中变量指向的值,此后 变量 指向的改变都不会影响 try 中 return 的返回
如果 finally 块中有 return 语句,则 try 或 catch 中的返回语句会被忽略
如果 finally 块中抛出异常,则整个 try、catch、finally块中抛出异常
【强制】不要在 finally 块中使用 return
The END.