深度剖析 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

相關文章
相關標籤/搜索