1. 程式人生 > >編寫高質量程式碼:改善Java程式的151個建議(第8章:異常___建議110~113)

編寫高質量程式碼:改善Java程式的151個建議(第8章:異常___建議110~113)

不管人類的思維有多麼縝密,也存在“智者千慮必有一失”的缺憾。無論計算機技術怎麼發展,也不可能窮盡所有的場景,這個世界是不完美的,是有缺陷的,完美的世界只存在於理想中。

對於軟體帝國的締造者來說,程式也是不完美的,異常情況會隨時出現,我們需要它為我們描述例外時間,需要它處理非預期的情景,需要它幫我們建立“完美世界”。

前言:淺談Java異常

1、在Java中,所有的異常都有一個共同的祖先Throwable(可丟擲)。

Throwable有兩個子類:Exception和error。

 Trowable類中常用方法如下:

1. 返回異常發生時的詳細資訊
public string getMessage();
 
2. 返回異常發生時的簡要描述
public string toString();
 
3. 返回異常物件的本地化資訊。使用Throwable的子類覆蓋這個方法,可以聲稱本地化資訊。如果子類沒有覆蓋該方法,則該方法返回的資訊與getMessage()返回的結果相同
public string getLocalizedMessage();
 
4. 在控制檯上列印Throwable物件封裝的異常資訊
public void printStackTrace();

2、異常分兩大類:

①執行時異常:都是RuntimeException類及其子類異常,如NullPointerException(空指標異常)、IndexOutOfBoundsException(下標越界異常)等,這些異常是不可查異常,這些異常一般由程式邏輯錯誤引起的,程式應該從邏輯角度儘可能避免這些異常的發生。執行時異常的特點是Java編譯器不會檢查它,也就是說,當程式中可能出現這類異常,即使沒有用try-catch語句捕獲它,也沒有用throws子句宣告丟擲它,也會編譯通過。

  通常,Java的異常(Throwable)分為可查的異常(checked exceptions)和不可查的異常(unchecked exceptions)。

① 可查異常(編譯器要求必須處置的異常):正確的程式在執行中,很容易出現的、情理可容的異常狀況。除了Exception中的RuntimeException及其子類以外,其他的Exception類及其子類(例如:IOException和ClassNotFoundException)都屬於可查異常。這種異常的特點是Java編譯器會檢查它,也就是說,當程式中可能出現這類異常,要麼用try-catch語句捕獲它,要麼用throws子句宣告丟擲它,否則編譯不會通過。

② 不可查異常(編譯器不要求強制處置的異常):包括執行時異常(RuntimeException與其子類)和錯誤(Error)。

3、異常處理的機制

① 丟擲異常:任何Java程式碼都可以丟擲異常。

② 捕獲異常:捕捉異常通過try-catch語句或者try-catch-finally語句實現。

finally 塊:無論是否捕獲或處理異常,finally塊裡的語句都會被執行。當在try塊或catch塊中遇到return語句時,finally語句塊將在方法返回之前被執行。在以下4種特殊情況下,finally塊不會被執行:
     1)在finally語句塊中發生了異常。
     2)在前面的程式碼中用了System.exit()退出程式。
     3)程式所在的執行緒死亡。
     4)關閉CPU。

應該在宣告方法丟擲異常還是在方法中捕獲異常?

捕捉並處理知道如何處理的異常,而丟擲不知道如何處理的異常。

總體來說,Java規定:對於可查異常必須捕捉、或者宣告丟擲。允許忽略不可查的RuntimeException和Error。

 

建議110:提倡異常封裝

建議111:採用異常鏈傳遞異常

建議112:可查異常儘可能轉化為不可查異常

建議113:不要在finally中處理返回值

建議110:提倡異常封裝

 Java語言的異常處理機制可以確保程式的健壯性,提高系統的可用率,但是Java API提供的異常都是比較低級別的,只有開發人員才能看的懂。而對於終端使用者來說,這些異常無異於天書,那該怎麼辦呢?這就需要我們對異常進行封裝。

異常封裝有三方面的有點:

1、提高系統的友好性

2、提高系統的可維護性

正確的做法是對異常進行分類處理,並進行封裝輸出,程式碼如下:  

public  void doStuff4(){
    try{
        //doSomething
    }catch(FileNotFoundException e){
        log.info("檔案未找到,使用預設配置檔案....");
        e.printStackTrace();
    }catch(SecurityException e1){
        log.info(" 無權訪問,可能原因是......");
        e1.printStackTrace();
    }
}

如此包裝後,維護人員看到這樣的異常就有了初步的判斷,或者檢查配置,或者初始化環境,不需要直接到程式碼層級去分析了。

3、解決Java異常機制自身的缺陷

Java中的異常一次只能丟擲一次,比如doStuff方法中有兩個邏輯程式碼片段,如果在第一個邏輯片段中丟擲異常,則第二個邏輯片段就不再執行了,也就無法丟擲第二個異常了,現在的問題是如何才能一次丟擲兩個或更多的異常呢?

其實,使用自行封裝的異常可以解決該問題,程式碼如下:

class MyException extends Exception {
    // 容納所有的異常
    private List<Throwable> causes = new ArrayList<Throwable>();

    // 建構函式,傳遞一個異常列表
    public MyException(List<? extends Throwable> _causes) {
        causes.addAll(_causes);
    }

    // 讀取所有的異常
    public List<Throwable> getExceptions() {
        return causes;
    }
}

MyException異常只是一個異常容器,可以容納多個異常,但它本身並不代表任何異常含義,它所解決的是一次丟擲多個異常的問題,具體呼叫如下:

public void doStuff() throws MyException {
    List<Throwable> list = new ArrayList<Throwable>();
    // 第一個邏輯片段
    try {
        // Do Something
    } catch (Exception e) {
        list.add(e);
    }
    // 第二個邏輯片段
    try {
        // Do Something
    } catch (Exception e) {
        list.add(e);
    }
    // 檢查是否有必要丟擲異常
    if (list.size() > 0) {
        throw new MyException(list);
    }
}

這樣一來,DoStuff方法的呼叫者就可以一次獲得多個異常了,也能夠為使用者提供完整的例外情況說明。可能有人會問:這種情況會出現嗎?怎麼回要求一個方法丟擲多個異常呢?

絕對有可能出現,例如Web介面註冊時,展現層依次把User物件傳遞到邏輯層,Register方法需要對各個Field進行校驗並註冊,例如使用者名稱不能重複,密碼必須符合密碼策略等,不要出現使用者第一次提交時系統顯示" 使用者名稱重複 ",在使用者修改使用者名稱再次提交後,系統又提示" 密碼長度小於6位 " 的情況,這種操作模式下的使用者體驗非常糟糕,最好的解決辦法就是異常封裝,建立異常容器,一次性地對User物件進行校驗,然後返回所有的異常。

建議111:採用異常鏈傳遞異常

正確的做法是先封裝再傳遞,步驟如下:

比如我們的JavaEE專案一般都有三層結構:持久層,邏輯層,展現層,持久層負責與資料庫互動,邏輯層負責業務邏輯的實現,展現層負責UI資料庫的處理。

1、把FIleNotFoundException封裝為MyException。

2、丟擲到邏輯層,邏輯層根據異常程式碼(或者自定義的異常型別)確定後續處理邏輯,然後丟擲到展現層。

3、展現層自行決定要展現什麼,如果是管理員則可以展現低層級的異常,如果是普通使用者則展示封裝後的異常。

在IOException的建構函式中,上一個層級的異常可以通過異常鏈進行傳遞,鏈中傳遞異常的程式碼如下所示:

try{
    //doSomething
}catch(Exception e){
    throw new IOException(e);
}

捕捉到Exception異常,然後將其轉化為IOException異常並丟擲(此方法叫異常轉譯),呼叫者獲得該異常後再呼叫getCause方法即可獲得Exception的異常資訊。

綜上所述,異常需要封裝和傳遞,我們在開發時不要“吞噬”異常,也不要赤裸裸的丟擲異常,封裝後再丟擲,或者通過異常鏈傳遞,可以達到系統更健壯,更友好的目的。

建議112:可查異常儘可能轉化為不可查異常

可查異常(Checked Exception)是正常邏輯的一種補償手段,特別是對可靠性要求比較高的系統來說,在某些條件下必須丟擲可查異常以便由程式進行補償處理,也就是說可查異常有存在的理由,那為什麼要把可查異常轉化為非=不可查異常呢?可查異常確實有不足的地方:

1、可查異常使介面宣告脆弱

我們要儘量多使用介面程式設計,可以提高程式碼的擴充套件性、穩定性,但是涉及異常問題就不一樣了,例如系統初期是一個介面是這樣設計的:

interface User{
    //修改使用者密碼,丟擲安全異常
    public void changePassword() throws MySecurityException;
}

可能有多個實現者,也可能丟擲不同的異常。

這裡會產生兩個問題:① 異常時主邏輯的補充邏輯,修改一個補充邏輯,就會導致主邏輯也被修改,也就會出現實現類“逆影響”介面的情景,我們知道實現類是不穩定的,而介面是穩定的,一旦定義異常,則增加了介面的不穩定性,這是面向物件設計的嚴重褻瀆;② 實現的變更最終會影響到呼叫者,破壞了封裝性,這也是淺談迪米特法則<最通俗易懂的講解>鎖不能容忍的。

2、可查異常使程式碼的可讀性降低

一個方法增加了可查異常,則必須有一個呼叫者對異常進行處理。

用try...catch捕捉異常,程式碼膨脹很多,可讀性也就降低了,特別是多個異常需要捕捉的時候,而且可能在catch中再次丟擲異常,這大大降低了程式碼的可讀性。

3、可查異常增加了開發工作量

我們知道異常需要封裝和傳遞,只有封裝才能讓異常更容易理解,上層模組才能更好的處理,可這會導致低層級的異常沒完沒了的封裝,無端加重了開發的工作量。

可查異常有這麼多的缺點,有什麼好的方法可以避免或減少這些缺點呢?就是將可查異常轉化為不可查異常,但是也不能把所有的異常轉化為不可查異常,有很多的未知不確定性。

我們可以在實現類中根據不同情況丟擲不同的異常,簡化了開發工作,提高了程式碼的可讀性。

那什麼樣的能轉化,什麼樣的不能轉化呢?

當可查異常威脅到系統額安全性、穩定性、可靠性、正確性,則必須處理,不能轉化為不可查異常,其它情況即可轉化為不可查異常。

建議113:不要在finally中處理返回值

1、覆蓋了try程式碼塊中的return返回值

public static int doStuff() {
    int a = 1;
    try {
        return a;
    } catch (Exception e) {
 
    } finally {
        // 重新修改一下返回值
        a = -1;
    }
    return 0;
}

該方法的返回值永遠是1,不會是-1或0(為什麼不會執行到" return 0 " 呢?原因是finally執行完畢後該方法已經有返回值了,後續程式碼就不會再執行了)

    public static Person doStuffw() {
        Person person = new Person();
        person.setName("張三");
        try {
            return person;
        } catch (Exception e) {    

        } finally {
            // 重新修改一下值
            person.setName("李四");
        }
        person.setName("王五");
        return person;
    }

此方法的返回值永遠都是name為李四的Person物件,原因是Person是一個引用物件,在try程式碼塊中的返回值是Person物件的地址,finally中再修改那當然會是李四了。

上面的兩個例子可以好好的琢磨琢磨!

2、遮蔽異常

public static void doSomeThing(){
    try{
        //正常丟擲異常
        throw new RuntimeException();
    }finally{
        //告訴JVM:該方法正常返回
        return;
    }
}

public static void main(String[] args) {
    try {
        doSomeThing();
    } catch (RuntimeException e) {
        System.out.println("這裡是永遠不會到達的");
    }
}

上面finally程式碼塊中的return已經告訴JVM:doSomething方法正常執行結束,沒有異常,所以main方法就不可能獲得任何異常資訊了。

這樣的程式碼會使可讀性大大降低,讀者很難理解作者的意圖,增加了修改的難度。

與return語句相似,System.exit(0)或RunTime.getRunTime().exit(0)出現在異常程式碼塊中也會產生非常多的錯誤假象,增加程式碼的複雜性,大家有興趣可以自行研究一下。

 

編寫高質量程式碼:改善Java程式的15