深度剖析 Java 的 try-catch 陷阱

1. 摘要

Java 的 try-catch 写法咱们常常用到,按照固定的方式咱们能够很容易捕获和处理异常:java

try {
    ....
} catch( e ) {
    ...
} finally {}
复制代码

可是,咱们会在某些写法上出现误区,这里将这些误区称为陷阱,本文会具体分析这些陷阱的产生原理,避免你们未来入坑。bash

2.例子1

//demo1
public static int testTryCatch() {
        try {
            int i = 1/0;// step1
        } catch(Exception e) { //2
            System.out.println("hello!");
            return 1;
        }finally{
            return 2;
        }
    }
复制代码

demo1 代码的 step1 位置,发生了个除以 0 的异常,按照代码的逻辑将走到 catch 代码块。catch 代码块执行了一个 return 指令返回了字面量 1。可是在 finally 语句块中也执行了一个 return 指令,那么最后函数将返回那个值呢?函数

咱们执行该函数将获得结果:ui

>"hello!"
>2
复制代码

也就是执行finally 语句块中的指令,这是为何呢?咱们来看下这段代码编译出来的字节码:spa

Code:
      stack=2, locals=1, args_size=0
         0: iconst_1
         1: iconst_0
         2: idiv
         3: istore_0
         4: goto          20
         7: astore_0
         8: getstatic     #16 // Field java/lang/System.out:Ljava/io/PrintStream;
        11: ldc           #22 // String hello!
        13: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        16: goto          20
        19: pop
        20: iconst_2
        21: ireturn
      Exception table:
         from    to  target type
             0     4     7   Class java/lang/Exception
             0    19    19   any
复制代码

上面 JVM 字节码中 0~2 行表明 try 语句块, 当执行到 idiv 除法指令的时候,出现了异常。这时候咱们须要查一下咱们的异常向量表,异常向量表包含两条数据:code

  1. 指令 0 到 4 行,捕获 Exception 类型数据,跳转到第 7 条指令
  2. 指令 0 到 19 行,捕获任何类型的 Throwable,跳转到 19 语句块
  • 咱们执行 try语句块中的 idiv 指令位于第 2 行,属于区间 [0,4] ,所以,当该语句发生异常的时候将执行向量表的第一条记录,跳转到代码 7 。
  • 代码 7 开始其实对应的是 catch 语句块, 这里将调用 astore_0 ,将一个引用对象(其实是一个 Exception 对象)存入局部变量表,而后执行 System.out.println 指令。以后,并无按照咱们的代码写法直接执行 return 1, return 指令被舍弃,而是跳转到 代码行 20 的位置,也就是 finally 代码块
  • 代码 20 行将存入一个字面量 2,而后执行 ireturn 指令返回。

所以,实际上编译器在编译咱们的 try-catch 语法代码的时候,当发现咱们的 finally 中有 return 语句的时候,将舍弃掉 catch 代码块中的 ireturn 指令对象

3.例子2

//code 2

public static int testFunc(MyObj obj) {
     try {
            int i = 1/0;
            return 10;
        } catch(Exception e) { //2
            obj.v= 2;
            return obj.v;
        }finally{
            obj.v= 3;
        }
}

复制代码

经过咱们对 例子1 的分析,咱们知道 finally 语句将会插入到 catch 语句的 return 代码以前。那么按照这种结论咱们的 例子2 就能够转化为:索引

catch(Exception e) {
    
    obj.v = 2;
    obj.v= 3;//finally 语句块
    return obj.v;
}
复制代码

也就是说 例子2 中函数的结果应该是: 3 。 可是,结论老是事与愿违的,实际上返回的是 2。咱们仍是看下它编译的字节码:ip

Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: iconst_0
         2: idiv //异常
         
         3: istore_1
         4: aload_0
         5: iconst_3
         6: putfield      #16 // Field david/support/Demos$MyObj.v:I
         9: bipush        10
        11: ireturn //catch 代码块
        
        12: astore_1
        
        13: aload_0
        14: iconst_2
        15: putfield      #16 // Field david/support/Demos$MyObj.v:I
        
        18: aload_0
        19: getfield      #16 // Field david/support/Demos$MyObj.v:I
        22: istore_3
        
        23: aload_0
        24: iconst_3
        25: putfield      #16 // Field david/support/Demos$MyObj.v:I
        
        28: iload_3
        29: ireturn
        
        30: astore_2
        31: aload_0
        32: iconst_3
        33: putfield      #16 // Field david/support/Demos$MyObj.v:I
        36: aload_2
        37: athrow
      Exception table:
         from    to  target type
             0     4    12   Class java/lang/Exception
             0     4    30   any
            12    23    30   any
复制代码
  • 代码执行到 try 语句块中的除法指令,对应代码第 2 行的时候发生的异常。 对应第 0 条向量表,跳转到 12 行 catch 语句块
  • 13 到 15 行,将字面量 2 存入局部变量 Demos$MyObj 对象的 v 属性中
  • 18 到 22 行,将局部变量 Demos$MyObj 对象的 v 属性,存入到索引 [3] 的局部变量中
  • 23 到 25 行,就是被插入的 finally 指令块,将对象的 v 属性变成 3
  • 28 到 29 行,将取出索引 [3] 的局部变量并返回

从上面的编译的代码咱们能够看到,例子2 中,finally 语句块中并无 return 指令的时候,编译器将会先把结果存入一个临时变量(上面的索引 [3] 局部变量)中,而后执行完 finally 语句后,读取临时变量中的值,执行 return 语句,而且,此时对象中的属性也由于执行了finally语句,而变成了 3 。get

相关文章
相关标签/搜索