Java異常處理機制難點解惑-用代碼說話

是否需要看這篇文章?

下面的例子中,如果正常執行返回值多少? 如果出現了ArithmeticException返回值多少? 如果出現非ArithmeticException(如NullPointerException)返回值多少?
如果你瞭解這個例子說明的問題,並瞭解例子中三種情況下的執行細節,這篇文章你就不用浪費時間看了。
例子:

    public int testException_finally(){
        int x;
        try {
            x = 1;
//int y = 1/0;  //放開此處,出現ArithmeticException。

/*//註釋掉 int y = 1/0;處,放開此處,出現NullPointerException
            String str = null; 
            str.substring(0);
            */
            return x;
        } catch (ArithmeticException e) {
            x =2;
            return x;
        } finally{
            x = 3;
        }
    }

答案:
如果正常執行,返回值爲1;
如果拋出ArithmeticException,返回值爲2;
如果拋出其他Exception,則拋出該Exception,無返回值。

一看就知曉的同學們,散了吧。這篇文章你們不需要看了。
不知所云的同學們,請繼續往下看。下面要說的正適合你☺

1. Java異常的分類

Throwable類是Java語言中所有異常和錯誤的基類。Throwable有兩個直接子類:Error和Exception。Error用來表示編譯期或系統錯誤,一般不用我們程序員關心(現在還是程序猿,但有一顆想做架構師的心☺);Exception是可以被拋出的異常的基本類型,這個纔是我們需要關心的基類。
Java異常在設計時的基本理念是用名稱代表發生的問題,異常的名稱應該可以望文知意。異常並非全是在java.lang包中定義,像IO異常就定義在java.io包中。在Exception中,有一種異常:RuntimeException(運行時異常)不需要我們在程序中專門捕獲,Java會自動捕獲此種異常,RuntimeException及其子類異常再加上Error異常,被統一叫做unchecked Exception(非檢查異常);其他的Exception及其子類異常(不包括RuntimeException及其子類異常),被統一叫做checked Exception(檢查型異常)。對於檢查型異常,纔是我們需要捕獲並處理的異常。非檢查型異常Java會自動捕獲並拋出。當然,我們也可以主動捕獲RuntimeException型異常。但是Error型異常一般不去捕獲處理。

2. Java異常處理的基本規則

對於可能發生異常的Java代碼塊,我們可以將其放入try{}中,然後在try之後使用catch(***Exception e){}處理拋出的具體異常,如果沒有匹配的catch處理拋出的異常,則會將該異常向上一層繼續拋出,直到拋至Main()方法。
有一些代碼,我們希望不管try中的代碼是成功還是失敗都需要執行,那麼這些代碼我們就可以放在finally{}中。
Java的異常處理採用的是終止模型,即如果try塊中的某處出現了異常,則立刻停止當前程序的運行,在堆中創建對應的異常對象,異常的處理轉入到異常處理代碼處(即對應的catch塊),執行完異常處理代碼後,try塊中出現異常處之後的程序將不會被執行,程序會跳出try塊,執行try塊之外的程序。
例子:覆蓋知識點:①執行對應的catch;②一定執行finally中代碼;③try出異常之後的代碼不再執行;

public String testException_one(){
        String str = "aaa";
        try {
            str += "bbb";
            int a = 1/0;
            str += "ccc";
        } catch (NullPointerException e) {
            str += "ddd";
        } catch (ArithmeticException e) {
            str += "eee";
        } finally {
            str += "fff";
        }
        str += "ggg";
        return str;
    }

程序執行返回結果:aaabbbeeefffggg
注意:沒有輸出ccc和ddd。
結果分析:上面的程序進入try塊後,連接了bbb,然後遇到1/0拋出ArithmeticException 異常,首先NullPointerException所在的catch塊不匹配該異常,然後檢查到ArithmeticException 所在的catch塊匹配該異常,進入該catch塊內進行異常處理。執行完所在的catch塊,一定會執行finally塊,但是try塊報異常行之後的代碼不會再執行,直接跳出try塊,繼續執行try…catch…finally之後的代碼。

3. 繼承和實現接口時的異常限制

class OneException extends Exception{}
class TwoException extends Exception{}
class OneSonException extends OneException{}
class TwoSonException extends TwoException{}

interface Worker {
    void work() throws TwoException;

    void say() throws OneException;
}

class Person {

    public Person() throws TwoException {
        System.out.println("Person Constructor...");
    }

    public void eat() throws OneException {
        System.out.println("Person eat...");
    }

    public void say() throws TwoException {
        System.out.println("Person say...");
    }

}

public class Coder extends Person implements Worker {

    /**
     * 此處的TwoException是必須的,因爲Person的構造函數中拋出了TwoException。
     * Coder在調用父類構造函數時,也必須拋出次異常,且不能是其子類異常.另外,構造函數可以拋出比父類多的異常。
     * @throws TwoException
     * @throws OneException
     */
    public Coder() throws TwoException, OneException {
        super();
    }

    /**
     * 實現的接口的方法或者重寫的父類的方法可以拋出原方法的異常或其子類異常或者不拋出異常,
     * 但是不能拋出原方法沒有聲明的異常。這樣是爲了多態時,當子類向上轉型爲基類執行方法時,基類的方法依然有效。
     */
    public void work() throws TwoSonException {
        // TODO Auto-generated method stub
    }

    /**
     * 在接口和父類中都有該方法,且異常聲明不是同一個異常,則該方法的聲明不能拋出任何異常,
     * 因爲子類中的該方法在多態時必須同時滿足其實現的接口和繼承的基類的異常要求。不能拋出比基類或接口方法聲明中更多的異常。
     */
    public void say(){

    }

    /**
     * 基類中eat方法拋出了異常,在子類中覆蓋該方法時,可以不聲明拋出異常
     */
    public void eat(){

    }
}
/**同時還應該注意,如果方法聲明拋出的是RunTimeException類型的異常,不受以上的限制;
只有檢查型異常才受以上限制。非檢查型異常由於系統自動捕獲,不受任何限制。
*
*/

4. finally一定會執行

①break/continue/while:如下面例子中所示在循環中遇到continue或break時,finally也會執行。

public void testException_two(){

        for(int i = 0; i < 5; i++){
            try {
                if(i == 0){
                    continue;
                }
                if(i == 1){
                    throw new Exception();
                }
                if(i == 3){
                    break;
                }
                System.out.println("try..." + i);
            } catch (Exception e) {
                System.out.println("catch..." + i);
            } finally {
                System.out.println("finally..." + i);
            }
        }

    }
    /*
    執行結果:
finally...0
catch...1
finally...1
try...2
finally...2
finally...3
         */

②return:即使在try塊中正常執行了return,finally也在return之前執行了。如下面例子所示:

    public void testException_three(){
        int a = 1;
        try {
            System.out.println("try...");
            return;
        } catch (Exception e) {
            // TODO: handle exception
        } finally{
            System.out.println("finally...");
        }
    }
    /*
執行結果:
try...
finally...
     */

③還有一種情況是:當try塊拋出異常時,如果沒有catch塊能捕獲到該異常,則該異常會被拋至上一級,在被拋至上一級之前,finally塊會被執行,然後異常纔會被拋至上一級。這個請有興趣的同學自己驗證吧。

總之,finally中的代碼是一定會被執行到的。

5. finally中丟失異常

因爲finally的特殊性,還會造成異常丟失的情況,如果在finally中拋出異常或者在finally中使用了return,則在try塊中拋出的異常將會被系統丟掉。如下面代碼所示(OneException和TwoException的定義在上面異常限制一節中已經給出):

    public void testException_finally_one(){
        try {
            System.out.println("test finally...");
            try {
                if(1 == 1){
                    throw new OneException();   
                }
            }finally{
                throw new TwoException();
            }
        } catch (Exception e) {
            System.out.println("e.getClass: " + e.getClass());
        }
    }
    /*
     * 
     執行結果輸出:
test finally...
e.getClass: class com.synnex.demo.TwoException
     */



    public void testException_finally_two(){
        try {
            System.out.println("test finally...");
            try {
                if(1 == 1){
                    throw new OneException();   
                }
            }finally{
                return;
            }
        } catch (Exception e) {
            System.out.println("e.getClass: " + e.getClass());
        }
    }

    /*
     執行結果輸出:
test finally...
     */

6. finally造成的返回值困惑

下面進入到本篇開始的那個例子的解惑。
例子:

    public int testException_finally(){
        int x;
        try {
            x = 1;
//int y = 1/0;  //放開此處,出現ArithmeticException。

/*//註釋掉 int y = 1/0;處,放開此處,出現NullPointerException
            String str = null; 
            str.substring(0);
            */
            return x;
        } catch (ArithmeticException e) {
            x =2;
            return x;
        } finally{
            x = 3;
        }
    }

答案:
如果正常執行,返回值爲1;
如果拋出ArithmeticException,返回值爲2;
如果拋出其他Exception,則拋出該Exception,無返回值。

解惑:這是我根據《深入理解Java虛擬機-JVM高級特性與最佳實踐》第二版書中的例子(P187~P188)做了一些修改。出現這種情況的原因是:在沒有出現異常的情況下,先執行了x=1;然後執行return x;時,首先是將x的一個副本保存在了方法棧幀的本地變量表中,執行return之前必須執行finally中的操作:x=3;將x的值設置爲了3,但是return時是將本地變量表中保存的x的那個副本拿出來放到棧頂返回。故沒出異常時,返回值爲1;出ArithmeticException異常或其子類異常時,返回值是2;如果出現非ArithmeticException異常,則執行完x=3之後,將異常拋出至上一層,沒有返回值。
對字節碼命令熟悉的朋友可以使用javap -verbose等命令反編譯出該方法的字節碼命令和異常表,從字節碼層面上就能清晰的看出執行過程了。我對字節碼命令知道得還不夠多,只能從大體上解釋這種運行過程。以後字節碼命令學得自認爲可以了的時候,也會寫字節碼相關的文章出來。希望這篇文章能幫到一些人理解Java的異常處理機制。

參考文章及書籍:
Java異常的深入研究與分析
《Java編程思想》第四版中文版第十二章通過異常處理錯誤
《深入理解Java虛擬機-JVM高級特性與最佳實踐》第二版 第六章類文件結構 周志明著

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章