Java例外處理規則

2020-07-16 10:05:15
前面介紹了使用例外處理的優勢、便捷之處,本節將進一步從程式效能優化、結構優化的角度給出例外處理的一般規則。成功的例外處理應該實現如下 4 個目標。
  1. 使程式程式碼混亂最小化。
  2. 捕獲並保留診斷資訊。
  3. 通知合適的人員。
  4. 採用合適的方式結束異常活動。

下面介紹達到這種效果的基本準則。

不要過度使用異常

不可否認,Java 的異常機制確實方便,但濫用異常機制也會帶來一些負面影響。過度使用異常主要有以下兩個方面:
  1. 把異常和普通錯誤混淆在一起,不再編寫任何錯誤處理程式碼,而是以簡單地丟擲異常來代替所有的錯誤處理。
  2. 使用例外處理來代替流程控制。

熟悉了異常使用方法後,程式設計師可能不再願意編寫煩瑣的錯誤處理程式碼,而是簡單地丟擲異常。實際上這樣做是不對的,對於完全已知和普通的錯誤,應該編寫處理這種錯誤處理程式碼,增加程式的健壯性;只有對外部的、不能確定和預知的執行時錯誤才使用異常。

下面我們來看《Java五子棋遊戲》一節中處理使用者輸入坐標點已有棋子的兩種方式。
// 如果使用者試圖下棋的坐標點已有棋子了
if (!gb.board[xPos - 1][yPos - 1].equals("╋")){
    System.out.println ("您輸入的坐標點已有棋子了,請重新輸入");
    continue;
}
上面這種處理方式檢測到使用者試圖下棋的坐標點已經有棋子了,立即列印一條提示語句,並重新開始下一次迴圈。這種處理方式簡潔明瞭,邏輯清晰,提高執行效率。

如果將上面的處理機制改為如下方式:
// 如果使用者試圖下棋的坐標點己經有棋子了,程式自行丟擲異常
if (!gb.board[xPos - 1][yPos - 1].equals ("╋")) {
    throw new Exception ("您試圖下棋的坐標點已經有棋子了");
}
上面的處理方式沒有提供有效的錯誤處理程式碼,當程式檢測到使用者試圖下棋的坐標點已經有棋子時,並沒有提供相應的處理,而是簡單的丟擲了一個異常。這種處理方式雖然簡單,但 Java 執行時接收到這個異常後,還需要進行相應的 catch 塊來捕獲該異常,所以執行效率要差一些。而且使用者下棋重複這個錯誤完全是預料的,所以程式完全可以針對該錯誤提供相應的處理,而不是丟擲異常。

例外處理機制的效率比正常的流程控制效率差,所以不要使用例外處理來代替正常的程式流程控制。例如,對於以下程式碼:
// 定義一個字串陣列
String[] arr = { "Hello", "Java", "Spring" };
// 使用例外處理來遍歷arr陣列的每個元素
try {
    int i = 0;
    while (true) {
        System.out.println(arr[i++]);
    }
} catch (ArrayIndexOutOfBoundsException ae) {
}
執行上面程式確實可以實現遍歷 arr 陣列元素的功能,但這種寫法可讀性較差,而且執行效率也不高。程式完全有能力避免產生 ArrayIndexOutOfBoundsException 異常,程式“故意”製造這種異常,然後使用 catch 塊去捕獲該異常,這是不應該的。將程式改為如下形式肯定要好得多。
String arr[] = {"Hello","Java","Spring"};
for (int i = 0; i < arr.length; i++) {
    System.out.println(arr[i]);
}
異常只應該用於處理非正常的情況,不要使用例外處理來代替正常的流程控制。對於一些完全可預知,而且處理方式清楚的錯誤,程式應該提供相應的錯誤處理程式碼,而不是將其籠統地稱為異常。

不要使用過於龐大的try塊

很多初學異常機制的讀者喜歡在 try 塊裡放置大量的程式碼,這樣看上去“很簡單“,但這種”簡單“只是一種假象,因為 try 塊裡的程式碼過於龐大,業務過於複雜,就會造成 try 塊中出現異常的可能性大大增加,從而導致分析異常原因的難度也大大增加。而且當 try 塊過於龐大時,就難免在 try 塊後緊跟大量的 catch 塊才可以針對不同的異常提供不同的處理邏輯。同一個 try 塊後緊跟大量的 catch 塊則需要分析它們之間的邏輯關係,反而增加了變成複雜度。

正確的做法是,把大塊的 try 塊分割成多個可能出現異常的程式段落,並把它們放在單獨的 try 塊中,從而分別捕獲並處理異常。

避免使用 Catch All 語句

所謂 Catch All 語句指的是一種異常捕獲模組,它可以處理程式發生的所有可能異常。例如,如下程式碼片段:
try {
    // 可能引發Checked異常的程式碼
} catch (Throwsble t) {
    // 進行例外處理
    t.printStackTrace();
}
不可否認,每個程式設計師都曾經用過這種例外處理方式,但在編寫關鍵程式時就應避免使用這種例外處理方式。這種處理方式有如下兩點不足之處。
  1. 所有的異常都採用相同的處理方式,這將導致無法對不同的異常分情況處理,如果要分情況處理,則需要在 catch 塊中使用分支語句進行控制,這是得不償失的做法。
  2. 這種捕獲方式可能將程式中的錯誤、Runtime 異常等可能導致程式終止的情況全部捕獲到,從而“壓制”了異常。如果出現了一些“關鍵”異常,那麼此異常也會被“靜悄悄”地忽略。

實際上,Catch All 語句不過是一種通過避免錯誤處理而加快程式設計進度的機制,應儘量避免在實際應用中使用這種語句。

不要忽略捕獲到的異常

不要忽略異常,既然已捕獲到異常,那 catch 塊理應處理並修復這個錯誤。catch 塊整個為空,或者僅僅列印出錯資訊都是不妥的。程式出了錯誤,所有的人都看不到任何異常,但整個應用可能已經徹底壞了,這是最可怕的事情。

對異常進行合適的修復,然後繞過異常發生的地方繼續執行;或者用別的資料進行計算,以代替期望的方法返回值;或者提示使用者重新操作等。總之,對於 Checked 異常,程式應該盡量修復。