跳至内容

拾光小记

怎么理解Try-Catch-Finally中的返回值

问题

有个常见又看似简单的问题:那就是当我们分别在JAVA的try,catch,finally返回值时,到底哪个是生效的,哪些代码被确实运行了?本文将通过JAVA字节码的方式为大家彻底解决这个问题。

先说结论

  1. finally代码会被强行插入到try和catch代码的后面,所以无论如何,finally都会被执行。
  2. 如果finally中执行了return,那么函数无论是否有异常,都会返回finally的返回值。所以,finally不要return!。
  3. 由于finally代码是插到了try和catch的代码块后面,所以finally的执行逻辑要在try和catch的后面。

看如下代码

public static void main(String[] args) {
        test();
    }

    public static int test() {
        try {
            System.out.println("try");
            return 1;
            
        } catch (Exception e) {
            System.out.println("catch");
            return 2;
            
        } finally {
            System.out.println("finally");
            return 3;
        }
    }
}

查看上述代码编译后的字节码

public class com.shein.luban.common.TryCatchTest {
  public com.shein.luban.common.TryCatchTest();
    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  #7                  // Method test:()I
       3: pop
       4: return

  public static int test();
    Code:
    ####################################### 执行try ######################################################
       0: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #19                 // String try
       5: invokevirtual #21                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: iconst_1

       ## 这里插入了finally中的代码块
      9: istore_0
      10: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
      13: ldc           #27                 // String finally
      15: invokevirtual #21                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      18: iconst_3
      19: ireturn

      ####################################### 执行catch ######################################################
      20: astore_0
      21: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
      24: ldc           #31                 // String catch
      26: invokevirtual #21                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      29: iconst_2

        ## 插入到catch中的finally代码块
      30: istore_1
      31: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
      34: ldc           #27                 // String finally
      36: invokevirtual #21                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      39: iconst_3
      40: ireturn

        ####################################### 执行finally ###################################################
      41: astore_2
      42: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
      45: ldc           #27                 // String finally
      47: invokevirtual #21                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      50: iconst_3
      51: ireturn


    ####################################### 异常表 ######################################################
    Exception table:
       from    to  target type
           0    10    20   Class java/lang/Exception
           0    10    41   any
          20    31    41   any
}

我们来逐步分析上述字节码,整个字节码按功能类型可以分为异常表,try主体,catch主体,finally主体。不过想要看懂上面的天书,还要学习几个简单的字节码指令。 字节码指令比较多,这里只介绍跟本文相关的指令

指令内循环操作次数
getstatic获取指定类的静态域,并将其值压入栈顶
一头雾水吧,这个咱不管,但是从上面代码大概能看出是获取PrintStream对象
ldcldc指令用来将常量池中指定的常量放入操作数栈中,从注释看,这玩意就是获取要输出的字符串
invokevirtual都invoke了,肯定是执行了方法。同理,注释中说是执行了PrintStream的println方法
iconst_n将int类型的变量值n放到栈顶,类比,有lconst_n等
istore_n/astore_n将栈顶的变量放到临时变量表中下标n的位置里
ireturn将栈顶的变量出栈返回

异常表

异常表有几个关键属性,分别如下: from: 从字节码哪里开始识别异常 to: 到字节码哪里结束识别异常 target: 遇到异常后,要跳转到哪里 type: 识别的异常类型是啥

整体解释如下 : 0~10: try主体代码,第一行第二行结合起来就是,如果在try中识别到了异常,并且类型是Exception就跳转到字节码第20行继续执行;
如果类型不是Exception,那么就跳转到第41行执行。
20行是catch开始的位置,41行是finally开始的位置。 总结就是如果代码识别的到的异常是Exception类型,就执行catch,否则就执行finally。是不是很简单?O(∩_∩)O~~

try代码块

####################################### 执行try ######################################################
       0: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #19                 // String try
       5: invokevirtual #21                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: iconst_1

       ## 这里插入了finally中的代码块
      9: istore_0
      10: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
      13: ldc           #27                 // String finally
      15: invokevirtual #21                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      18: iconst_3
      19: ireturn

0~5输出字符串try,第8行将int 1 压入栈顶。第9行将栈顶的1放到临时变量表的0号下标。然后又执行finally里面的System.out.println,18行将int值3压入栈顶,然后执行ireturn,将3弹出栈顶并返回。
人话讲就是:上面代码没有抛出异常,最终会返回finally的返回值。

catch代码块

####################################### 执行catch ######################################################
      20: astore_0
      21: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
      24: ldc           #31                 // String catch
      26: invokevirtual #21                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      29: iconst_2

        ## 插入到catch中的finally代码块
      30: istore_1
      31: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
      34: ldc           #27                 // String finally
      36: invokevirtual #21                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      39: iconst_3
      40: ireturn

20行标识将栈顶的变量值放到临时变量表的0号位置,然后输入字符串catch,接下来将int元素2放到栈顶。随后,代码再次进入finally代码块中,先将栈顶的元素2放到临时变量表的1号位置。输入finally字符串,将int元素3压入栈顶,并执行ireturn弹出栈顶元素3后返回。
如果catch执行过程中没有抛出异常,那么代码执行完毕后。局部变量栈中只有2,局部变量表是[null, 2]。

finally代码块

####################################### 执行finally ######################################################
      41: astore_2
      42: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
      45: ldc           #27                 // String finally
      47: invokevirtual #21                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      50: iconst_3
      51: ireturn

这个代码不做过多解释了,执行完后弹出栈顶元素3并返回。
如果finally执行没有异常,那么在本地变量表可能是[1, null, null]或者[null, 2, null]