1. 程式人生 > >學習筆記(五):Java異常機制

學習筆記(五):Java異常機制

主要從這幾個方面來說說Java中的異常:

圖1.知識體系

1.  異常:指的是程式在執行過程中,出現的非正常的情況,最終會導致JVM的非正常停止。

     異常的繼承體系如下:

圖1.1 異常的繼承體系

    Throwable類是 Java 語言中所有錯誤或異常的父類。所以,如果要實現自定義異常,那麼就要繼承這個類或其子類。

    Error:不做過多陳述,出現錯誤是非常嚴重的,因為Error是無法處理(虛擬機器相關的問題:系統崩潰,虛擬機器錯誤,記憶體空間不足,方法呼叫棧溢)必須找到導致錯誤的根源,修改程式碼。

    Exception:異常,也就是平時所說的“報錯”,具體表現就是IDE控制檯出現紅色的資訊,異常類以......Exception結尾,他有非常多的子類。

如圖,我們都知道除數不能為0,當除數為0時,就會有一個算術異常(java.lang.ArithmeticException: / by zero):

圖2 異常示例  

    異常的分類:如圖2所示,在我們編寫程式碼時,並沒有出現任何錯誤提示,而當啟動程式執行時,出現了這個異常。

我們把這個叫做:執行時異常(runtimeException) ;當寫程式碼的過程中出現錯誤提示(紅色波浪線),稱為:編譯錯誤。

常見的執行異常有1. NullPointerExceptin 空指標     2. IndexOutOfBoundsException 索引越界        3. ClassCastException 類轉換      4. IllegalArgumentException 非法引數

    問題:異常是怎麼產生的(異常產生的過程)?

簡單說一下,異常產生的過程。我們都知道,java程式都是經過編譯後,執行在JVM(Java虛擬機器)上,如圖2,程式碼經過編譯為.class檔案,JVM載入.class檔案開始執行:①JVM檢測到程式做1除以0運算,但除數不能為0,於是,JVM建立了異常物件:exception = new ArithmeticException();   ②丟擲異常物件(throw exception); ③把異常傳遞給方法呼叫者(這裡為main),但main並沒有處理這個異常,又傳給了JVM;④JVM處理異常簡單粗暴:列印異常資訊,然後終止程式執行。

    問題:以圖2為例,JVM處理異常簡單粗暴,那麼我們能否在JVM處理前,自己把這個異常丟擲?

當然可以,我們可以在做運算前檢查一下引數,這裡主要檢查引數b。如果有問題自己在方法內丟擲異常。程式碼如下:

public static void main(String[] args) {
        int a = 1,b = 0;
        getResult(a, b);
    }
    //計算a除以b的結果的方法
    private static void getResult(int a, int b) {
        //檢查引數
        if (b == 0){
            //仿照JVM內,丟擲(throw)一個(宣告好的)算數異常
            throw new ArithmeticException("b 不能為0");
        }
        int  i = a/b;
        System.out.println(i);
    }

2. 那麼,該如何在出現異常時優雅的處理異常,讓程式不停止執行或者奔潰?

處理異常,離不開五個關鍵字:try、catch、finally、throw、throws。

    ①先說第一個關鍵字,throw(丟擲):這個關鍵字,在上述示例中已經出現。有關throw關鍵字的使用:throw是使用在方法內,將這個異常傳遞(動態的丟擲)到物件呼叫者處,並停止當前方法的執行(後面的方法也不會執行)。       

格式:throw new 異常類名(引數)。   如:throw new NullPointerException("要訪問的值不存在");

作用: 警告方法呼叫者,非法呼叫當前方法。

原始碼解讀_Objects.requireNonNull()方法: 這是java.util.Objects工具類的一個檢視指定引用物件不為空的靜態方法,該方法主要用於驗引數驗證和建構函式。

public static <T> T requireNonNull(T obj) {
        if (obj == null)
            throw new NullPointerException();
        return obj;
    }

如果這個方法用於圖2的引數驗證和我們自己丟擲異常效果是一樣的,只不過直接使用,更方便。

②第二個關鍵字,throws(丟擲):這裡準確說為宣告異常,即,將可能會出現的問題標識出來,報告給方法呼叫者,由呼叫者處理。【如果說異常是一個“流氓”,那麼throws的處理方式為,我打不過你,我去叫人處理你;而稍後要說的try...catch,則一點“不慫”,直接踹你,把你KO了】 

   用法:運用於方法宣告之上,用於表示當前方法不處理異常,而是提醒該方法的呼叫者來處理異常(丟擲異常).

   格式:方法名(引數)throws  異常類名1,異常類名2......{//方法體}

    示例稍後展示;

③try...catch...(catch...)finally:

如果發生異常,不處理的話,程式就會立刻停止,這樣帶來的後果是很嚴重的,所以,我們要處理異常,即,用try..catch...

try中寫的是可能會出現異常的程式碼,catch來捕獲異常,當try中的程式碼出現問題時,catch就會立刻做出響應,捕獲並處理異常;反之,try中的程式碼正常執行,也就沒catch什麼事了。

語法 (try和catch都不能單獨使用,必須連用):

try{     //編寫可能會出現異常的程式碼 }catch(異常型別 e){     //處理異常的程式碼     //記錄日誌/列印異常資訊/繼續丟擲異常 }

 public static void main(String[] args) {
        method();
        System.out.println("程式執行結束了");

    }
    private static void method() {
        int[] array = {1,2,3};
        try{
            int e = array[3]; //JVM : throw new ArrayIndexOutOfBoundsException()
            System.out.println(e);
        }catch (Exception e){ // e = new ArrayIndexOutOfBoundsException()
            // 發生異常並捕獲了,執行這裡
            System.out.println("呵呵,異常發生了");
        }
    }

這樣雖然捕獲了異常,並且不影響後面的程式碼執行,但是,如果業務邏輯複雜,要想快速定位到發生異常的原因,位置,就要呼叫printStackTrace()方法,這個方法的作用是列印棧中追溯,簡單說,就是列印異常資訊(包含了異常的型別,原因和出現的位置)。只需要在catch中加入:

// 列印棧中追溯 (列印異常資訊)
e.printStackTrace();

那麼上面說的throws,將異常宣告,由方法呼叫者處理,那麼,方法呼叫者就需要try...catch來處理。

例:

public static void main(String[] args) {
        String date = "2018-09-09";     //定義個時間,這裡格式是正確的,可以試試錯的
        //呼叫方法,需要處理這個方法丟擲的異常
        try {
            method(date);
        } catch (ParseException e) {
            e.printStackTrace();
        }

    }
    //將字串型別時間轉換成Date型別
    private static void method(String date) throws ParseException {
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        //parse 方法可能有一個格式轉換的問題,也就是引數格式個定義的轉換格式不一致
        //這裡不處理,而是丟擲
        Date date1 = dateFormat.parse(date);
        System.out.println(date1);
    }

那麼當,try中的程式碼可能發生多種異常時,那麼可能就需要“分類討論”,也就是分別列出可能發生異常情況;

語法:

try{     //編寫可能會出現異常的程式碼 }catch(異常型別 e){     //處理異常的程式碼     //記錄日誌/列印異常資訊/繼續丟擲異常 } catch (異常型別  e){

}

//以下程式碼只是示例,不具有任何實際意義 
public static void main(String[] args) {
        try {
            method(1);
        } catch (NullPointerException e) {
            // 如果出現空指標異常
            e.printStackTrace();
            System.out.println(0);
        }catch (IndexOutOfBoundsException e) {
            //如果出現越界異常
            e.printStackTrace();
            System.out.println(1);
        }
        System.out.println("程式繼續執行");
    }
    private static void method(int i) {
        if(i == 0){
            throw new NullPointerException("空指標異常");
        }else{
            throw new ArrayIndexOutOfBoundsException("越界異常了");
        }
    }

    我們都知道Exception是所有異常的父類,可不可以直接用一個catch(Exception e)處理呢?當然是可以的。那麼,這樣“分類討論”有什麼意義呢?

    當Java執行時,出現異常,就需要處理,而異常種類又是多種多樣的,不同的異常,根據實際業務需求處理的方式可能不同,所以,需要不同的catch塊來處理專門的異常。【多個catch塊處理時,需要,注意異常等級需要從小到大,也就是第一個catch處理的異常一定不能是他的父類,因為父類異常一定能處理子類的異常,要是父類異常都處理完了,要子類異常來幹嘛!】

最後來說說,finally程式碼塊,finally意為最後的,在這裡的意思就是,無論這個這個程式碼try了,還是catch了,finally都要執行。

  public static void main(String[] args) {

        try{
            //可能出現異常的程式碼
            int i = 1/0;
            System.out.println(i);
        }catch(NullPointerException e){
            // 抓到異常,會執行這裡
            System.out.println("抓住了");

        }finally {
            // 無論如何都執行
            System.out.println("無論如何都執行");
        }

        System.out.println("程式繼續執行");
    }