1. 程式人生 > >Java中try catch finally語句中含return語句的執行情況總結-程式設計陷阱

Java中try catch finally語句中含return語句的執行情況總結-程式設計陷阱

前言:有java程式設計基礎的人對java的異常處理機制都會有一定了解,而且可能感覺使用起來也比較簡單,但如果在try catch finally語句塊中遇到return語句,開發者可能就會遇到一些邏輯問題,甚至步入程式設計的陷阱。不信,我們先看看一段小程式,讀者可以分析其邏輯然後猜測其輸出結果:

public class Test {
    public static void main(String[] args) {
        Test t = new Test();
        System.out.println(t.Test1());
    }

    public
boolean Test1() { boolean b = true; try { int i = 10 / 0; System.out.println("i = " + i); return true; } catch (Exception e) { System.out.println(" -- catch --"); System.out.println("b:" + b); return b = false
; } finally { System.out.println(" -- finally --"); System.out.println("b:" + b); } } }

請先停止繼續閱讀,試著說出其執行結果。
如果你不能很自信地正確說出這段程式的執行邏輯和結果,那本文就值得你一讀,如果你可以,請忽略本文。
執行結果是:
– catch –
b:true
– finally –
b:false
false
你說對了嗎?

正文
首先我們瞭解java的異常處理機制中try、catch、finally的基本執行邏輯。

  • try:包裹可能引發異常的程式碼
  • catch:可以有多個catch塊,一個程式碼塊對應一種異常型別,表明該catch塊用於處理此型別的異常。
  • finally:主要用於回收在try塊裡使用的物理資源(比如資料庫連線、網路連線和磁碟檔案等),這些物理資源必須顯示回收,因為java的垃圾回收機制不會回收任何的物理資源,垃圾回收機制只回收堆記憶體中物件所佔用的記憶體。異常機制保證finally塊內的程式碼總是被執行,除非在try塊或者catch塊中呼叫了退出虛擬機器的方法(即System.exit(1);),此時程式直接退出,不再執行finally塊。

首先明確其語法結構:
1. try塊必存在, catch塊和finally塊都是可選的,但兩者需至少出現其中之一,也可以同時出現;
2. 可以有多個catch塊,捕獲父類異常的catch塊必須位於捕獲子類異常的後面,即設計多個catch塊捕獲異常時要先捕獲小的異常,再捕獲大的異常。一般,try塊後只有一個catch塊會被執行,絕不可能有多個catch塊被執行,除非使用continue開始下次迴圈時,再次執行到這個try、catch塊,而捕獲處理其他catch的異常;
3. 多個catch塊必須位於try塊之後,finally塊必須位於所有catch塊之後。

下面我們分情況討論try、catch、finally中含有return的情況。
示例一(try、catch、finally中都有return):

public class Test {
    public static void main(String[] args) {
        Test t = new Test();
        System.out.println(t.Test1());
    }

    public boolean Test1() {
        try {
            int i = 10 / 0; // 丟擲 Exception,try中其他程式碼不執行,進入catch
            System.out.println("i = " + i);
            return true; 
        } catch (Exception e) {
            System.out.println(" -- catch --");
            return false;
        } finally {
            System.out.println(" -- finally --");
            return true;
        }
    }
}

Eclipse中,這段程式碼會出警告:finally block does not complete normally。譯為:finally塊不能正常完成。Java不建議在finally塊中使用renturn或throw等導致方法終止的語句,否則將會導致try塊、catch塊中的return、throw語句失效。但是仍然可以執行,結果為:
– catch –
– finally –
true
這裡最後列印的true即時finally中返回的true。至於為什麼會覆蓋,這個涉及到JVM底層位元組碼的具體實現和一些指令操作,如果沒有JVM和計算機組成原理以及作業系統的相關基礎知識是較難以理解的,有餘力的開發者可以深入研究。

例項二(try、catch中有return,finally中無):

public class Test {
    public static void main(String[] args) {
        Test t = new Test();
        System.out.println(t.Test1());
    }

    public boolean Test1() {
        try {
            int i = 10 / 0; // 丟擲 Exception,try中其他程式碼不執行,進入catch
            System.out.println("i = " + i);
            return true; // 不會執行
        } catch (Exception e) {
            System.out.println(" -- catch --");
            return false; // Exception 丟擲,獲得了返回false的機會
        } finally {
            System.out.println(" -- finally --");
        }
    }
}

執行結果:
– catch –
– finally –
false

斷點除錯可發現其邏輯:在finally執行後,又回到catch語句裡面的return上,然後返回這句中的false。然後,很多人就會認為,甚至很多技術部落格都寫到:只有finally塊執行完成之後,才會回來執行try或者catch塊中的return語句。 但是!!真的是這樣嗎?也許是說這話的人知道意思但表達不夠清楚,以至於讓筆者覺得是誤人子弟,try、catch中的rerun語句的邏輯是否執行?執行完finally塊後一定會返回去執行try、catch嗎?我們回過頭來看開篇的那段程式碼:

public class Test {
    public static void main(String[] args) {
        Test t = new Test();
        System.out.println(t.Test1());
    }
    public boolean Test1() {
        boolean b = true;
        try {
            int i = 10 / 0; 
            System.out.println("i = " + i);
            return true; 
        } catch (Exception e) {
            System.out.println(" -- catch --");
            System.out.println("b:" + b);
            return b = false; 
        } finally {
            System.out.println(" -- finally --");
            System.out.println("b:" + b);
        }
    }
}

程式碼中我們將catch中的return語句加上了一個賦值操作,斷點除錯,可以發現,程式執行到int i = 10 / 0; 語句後跳出catch塊,並完整執行到return b = false; 然後進入finally塊,執行兩條輸出語句,第二條System.out.println(“b:” + b);這裡的b變成了false!,說明之前return中的賦值語句有執行,執行完finally後,程式再次進入catch中的return,返回給呼叫者。所以事實是,當Java程式執行try塊、catch塊遇到return語句時,當系統執行完return語句之後,並不會立即結束該方法,而是去尋找該異常處理流程中是否包含finally塊,如果沒有finally塊,方法終止,返回相應的返回值。如果有finally塊,系統立即開始執行finally塊——只有當finally塊執行完成後,系統才會再次跳回來根據return語句結束方法。如果finally塊裡使用了return語句來導致方法結束,則finally塊已經結束了方法,系統不會跳回去執行try、catch塊裡的任何程式碼。

依照此標準,下列程式應該輸出什麼?

public class Test {
    public static void main(String[] args) {
        Test t = new Test();
        System.out.println(t.test());
    }

    public static int test() {
        int count = 5;
        try {
            return ++count;
        } catch (Exception e) {
            // TODO: handle exception
        } finally {
            System.out.println("finally()執行");
            return count++;
        }
    }
}

答案是:
finally()執行
6

測驗:

public class Test {
    public static void main(String[] args) {
        int a  = test();
        System.out.println(a);
    }

    public static int test() {
        int count = 5;
        try {
            throw new RuntimeException("測試異常1");
        }catch (RuntimeException e) {
            System.out.println(e.toString());
            throw new RuntimeException("測試異常2");
        }catch (Exception e) {
            System.out.println(e.toString());
            return 2;
        } finally {
            System.out.println("finally()執行");
            return count;
        }
    }
}

執行結果:
java.lang.RuntimeException: 測試異常1
finally()執行
5

如果註釋掉語句:return count;
則執行結果是先打印出
“java.lang.RuntimeException: 測試異常1
finally()執行”
後再丟擲測試異常2,程式異常結束。

簡而言之,程式在catch中執行throw語句時並不會立即丟擲異常,而是去尋找該異常處理流中是否包含finally塊。如果沒有finally塊,程式立即丟擲異常;如果有finally塊,程式立即開始執行finally塊——只有當finally塊執行完成後,系統才會再次跳回來丟擲異常。如果finally塊裡使用return語句來結束方法,系統將不會跳回catch塊去丟擲異常。

如果去掉catch塊和finally中的return語句,則執行結果為:
先列印“System.out.println(“finally()執行”);”再丟擲異常1,方法異常結束。
如果去掉catch塊,保留finally中的return語句,則執行結果為:
“finally()執行
5”
方法正常結束,不會再丟擲異常。

實踐是檢驗真理的唯一標準。學會用質疑的眼光求真,用踏實的態度去實踐,是每一個成熟的技術人員所必須的。