程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> 編寫高質量代碼:改善Java程序的151個建議(第8章:異常___建議110~113),java151

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

編輯:JAVA綜合教程

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


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

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

建議110:提倡異常封裝

  Java語言的異常處理機制可以去確保程序的健壯性,提高系統的可用率,但是Java API提供的異常都是比較低級的(這裡的低級是指 " 低級別的 " 異常),只有開發人員才能看的懂,才明白發生了什麼問題。而對於終端用戶來說,這些異常基本上就是天書,與業務無關,是純計算機語言的描述,那該怎麼辦?這就需要我們對異常進行封裝了。異常封裝有三方面的優點:

(1)、提高系統的友好性

  例如,打開一個文件,如果文件不存在,則回報FileNotFoundException異常,如果該方法的編寫者不做任何處理,直接拋到上層,則會降低系統的友好性,代碼如下所示:

public static void doStuff() throws FileNotFoundException {
        InputStream is = new FileInputStream("無效文件.txt");
        /* 文件操作 */
    }

  此時doStuff的友好性極差,出現異常時(如果文件不存在),該方法直接把FileNotFoundException異常拋到上層應用中(或者是最終用戶),而上層應用(或用戶要麼自己處理),要麼接著拋,最終的結果就是讓用戶面對著" 天書 " 式的文字發呆,用戶不知道這是什麼問題,只是知道系統告訴他"  哦,我出錯了,什麼錯誤?你自己看著辦吧 "。

  解決辦法就是封裝異常,可以把異常的閱讀者分為兩類:開發人員和用戶。開發人員查找問題,需要打印出堆棧信息,而用戶則需要了解具體的業務原因,比如文件太大、不能同時編寫文件等,代碼如下: 

public static void doStuff2() throws MyBussinessException{
        try {
            InputStream is = new FileInputStream("無效文件.txt");
        } catch (FileNotFoundException e) {
            //方便開發人員和維護人員而設置的異常信息
            e.printStackTrace();
            //拋出業務異常
            throw new MyBussinessException();
        }
        /* 文件操作 */
    }

(2)、提高系統的可維護性

  看如下代碼: 

public  void doStuff3(){
        try{
            //doSomething
        }catch(Exception e){
            e.printStackTrace();
        }
        
    }

  這是大家很容易犯的錯誤,拋出異常是吧?分類處理多麻煩,就寫一個catch塊來處理所有的異常吧,而且還信誓旦旦的說" JVM會打印出棧中的錯誤信息 ",雖然這沒錯,但是該信息只有開發人員自己看的懂,維護人員看到這段異常時基本上無法處理,因為需要到代碼邏輯中去分析問題。

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

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:采用異常鏈傳遞異常

  設計模式中有一個模式叫做責任鏈模式(Chain of Responsibility) ,它的目的是將多個對象連成一條鏈,並沿著這條鏈傳遞該請求,直到有對象處理它為止,異常的傳遞處理也應該采用責任鏈模式。

  上一建議中我們提出了異常需要封裝,但僅僅封裝還是不夠的,還需要傳遞異常。我們知道,一個系統友好性的標志是用戶對該系統的" 粘性",粘性越高,系統越友好,粘性越低系統友好性越差,那問題是怎麼提高系統的“粘性”呢?友好的界面和功能是一個方面,另外一個方面就是系統出現非預期情況的處理方式了。

  比如我們的JavaEE項目一般都有三層結構:持久層,邏輯層,展現層,持久層負責與數據庫交互,邏輯層負責業務邏輯的實現,展現層負責UI數據庫的處理,有這樣一個模塊:用戶第一次訪問的時候,需要從持久層user.xml中讀取信息,如果該文件不存在則提示用戶創建之,那問題來了:如果我們直接把持久層的異常FileNotFoundException拋棄掉,邏輯層根本無法得知發生了何事,也就不能為展現層提供一個友好的處理結果了,最終倒霉的就是發展層:沒有辦法提供異常信息,只能告訴用戶說“出錯了,我也不知道出什麼錯了”___毫無友好性可言。

  正確的做法是先封裝,然後傳遞,過程如下:

(1)、把FIleNotFoundException封裝為MyException。

(2)、拋出到邏輯層,邏輯層根據異常代碼(或者自定義的異常類型)確定後續處理邏輯,然後拋出到展現層。

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

  明白了異常為什麼要傳遞,那接著的問題就是如何傳遞了。很簡單,使用異常鏈進行異常的傳遞,我們以IOException為例來看看是如何傳遞的,代碼如下:

public class IOException extends Exception {

    public IOException() {
        super();
    }
    //定義異常原因
    public IOException(String message) {
        super(message);
    }
    //定義異常原因,並攜帶原始異常
    public IOException(String message, Throwable cause) {
        super(message, cause);
    }
    //保留原始異常信息
    public IOException(Throwable cause) {
        super(cause);
    }
}

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

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

  捕捉到Exception異常,然後把它轉化為IOException異常並拋出(此種方式也叫作異常轉譯),調用者獲得該異常後再調用getCause方法即可獲得Exception的異常信息,如此即可方便地查找到產生異常的基本信息,便於解決問題。

  結合上一建議來說,異常需要封裝和傳遞,我們在進行系統開發時不要" 吞噬 " 異常,也不要赤裸裸的拋出異常,封裝後再拋出,或者通過異常鏈傳遞,可以達到系統更健壯,更友好的目的。

建議112:受檢異常盡可能轉化為非受檢異常

  為什麼說是" 盡可能"的轉化呢?因為" 把所有的受檢異常(Checked Exception)"都轉化為非受檢異常(Unchecked Exception)" 這一想法是不現實的:受檢異常是正常邏輯的一種補償手段,特別是對可靠性要求比較高的系統來說,在某些條件下必須拋出受檢異常以便由程序進行補償處理,也就是說受檢異常有合理存在的理由,那為什麼要把受檢異常轉化為非受檢異常呢?難道說受檢異常有什麼缺陷或者不足嗎?是的,受檢異常確實有不足的地方:

(1)、受檢異常使接口聲明脆弱

  OOP(Object Oriented Programming,面向對象程序設計) 要求我們盡量多地面向接口編程,可以提高代碼的擴展性、穩定性等,但是涉及異常問題就不一樣了,例如系統初期是這樣設計一個接口的:  

interface User{
    //修改用戶密碼,拋出安全異常
    public void changePassword() throws MySecurityException;
}

  隨著系統的開發,User接口有了多個實現者,比如普通的用戶UserImpl、模擬用戶MockUserImpl(用作測試或系統管理)、非實體用戶NonUserImpl(如自動執行機,邏輯處理器等),此時如果發現changePassword方法可能還需要拋出RejectChangeException(拒絕修改異常,如自動執行正在處理的任務時不能修改其代碼),那就需要修改User接口了:changePassword方法增加拋出RejectChangeException異常,這會導致所有的User調用者都要追加了對RejectChangeException異常問題的處理。

  這裡產生了兩個問題:一、 異常是主邏輯的補充邏輯,修改一個補充邏輯,就會導致主邏輯也被修改,也就是出現了實現類 " 逆影響 " 接口的情景,我們知道實現類是不穩定的,而接口是穩定的,一旦定義了異常,則增加了接口的不穩定性,這是面向對象設計的嚴重亵渎;二、實現的變更最終會影響到調用者,破壞了封裝性,這也是迪米特法則所不能容忍的。

(2)、受檢異常使代碼的可讀性降低

  一個方法增加可受檢異常,則必須有一個調用者對異常進行處理,比如無受檢異常方法doStuff是這樣調用的:

public static void main(String[] args) {
        doStuff();
    }

  doStuff方法一旦增加受檢異常就不一樣了,代碼如下: 

public static void main(String[] args) {
        try{
            doStuff();
        }catch(Exception e){
            e.printStackTrace();
        }
    }

  doStuff方法增加了throws Exception,調用者就必須至少增加4條語句來處理該異常,代碼膨脹許多,可讀性也降低了,特別是在多個異常需要捕捉的情況下,多個catch塊多個異常處理,而且還可能在catch塊中再次拋出異常,這大大降低了代碼的可讀性。

(3)、受檢異常增加了開發工作量

  我們知道,異常需要封裝和傳遞,只有封裝才能讓異常更容易理解,上層模塊才能更好的處理,可這會導致低層級的異常沒玩沒了的封裝,無端加重了開發的工作量。比如FileNotFoundException進行封裝,並拋出到上一個層級,於是增加了開發工作量。

  受檢異常有這麼多的缺點,那有沒有什麼方法可以避免或減少這些缺點呢?有,很簡單的一個規則:將受檢異常轉化為非受檢異常即可,但是我們也不能把所有的受檢異常轉化為非受檢異常,原因是在編碼期上層模塊不知道下層模塊會拋出何種非受檢異常,只有通過規則或文檔來描述,可以這樣說: 

  • 受檢異常提出的是" 法律下的自由 ",必須遵守異常的約定才能自由編寫代碼。
  • 非受檢異常則是“ 協約性質的自由 ”,你必須告訴我你要拋什麼異常,否則不會處理。

  以User接口為例,我們在聲明接口時不再聲明異常,而是在具體實現時根據不同的情況產生不同的非受檢異常,這樣持久層和邏輯層拋出的異常將會由展現自行決定如何展示,不再受異常的規則約束了,大大簡化開發工作,提高了代碼的可讀性。

  那問題又來了,在開發和設計時什麼樣的受檢異常有必要化為非受檢異常呢?" 盡可能 " 是以什麼作為判斷依據呢?受檢異常轉換為非受檢異常是需要根據項目的場景來決定的,例如同樣是刷卡,員工拿著自己的工卡到考勤機上打考勤,此時如果附近有磁性物質干擾,則考勤機可以把這種受檢異常轉化為非受檢異常,黃燈閃爍後不做任何記錄登記,因為考勤失敗這種情景不是" 致命 "的業務邏輯,出錯了,重新刷一下即可。但是到銀行網點取錢就不一樣了,拿著銀行卡到銀行取錢,同樣有磁性物質干擾,刷不出來,那這種異常就必須登記處理,否則會成為威脅銀行卡安全的事件。匯總成一句話:當受檢異常威脅到了系統的安全性,穩定性,可靠性、正確性時,則必須處理,不能轉化為非受檢異常,其它情況則可以轉化為非受檢異常。

  注意:受檢異常威脅到系統的安全性,穩定性、可靠性、正確性時,不能轉換為非受檢異常。

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

  在finally代碼塊中處理返回值,這是考試和面試中經常出現的題目。雖然可以以此來出考試題,但在項目中絕對不能再finally代碼塊中出現return語句,這是因為這種處理方式非常容易產生" 誤解 ",會誤導開發者。例如如下代碼:  

public class Client113 {
    public static void main(String[] args) {
        try {
            System.out.println(doStuff(-1));
            System.out.println(doStuff(100));
        } catch (Exception e) {
            System.out.println("這裡是永遠不會到達的");
        }
    }
    //該方法拋出受檢異常
    public static int doStuff(int _p) throws Exception {
        try {
            if (_p < 0) {
                throw new DataFormatException(" 數據格式錯誤 ");
            } else {
                return _p;
            }

        } catch (Exception e) {
            // 異常處理
            throw e;
        } finally {
            return -1;
        }
    }
}

  對於這段代碼,有兩個問題:main方法中的doStuff方法的返回值是什麼?doStuff方法永遠都不會拋出異常嗎?

  答案是:doStuff(-1)的值是-1,doStuff(100)的值也是-1,調用doStuff方法永遠都不會拋出異常,有這麼神奇?原因就是我們在finally代碼塊中加入了return語句,而這會導致出現以下兩個問題:

(1)、覆蓋了try代碼塊中的return返回值

  當執行doStuff(-1)時,doStuff方法產生了DataFormatException異常,catch塊在捕捉此異常後直接拋出,之後代碼執行到finally代碼塊,就會重置返回值,結果就是-1了。也就是出現先返回,再重置返回的情況。

  有人可能會思考,是不是可以定義變量,在finally中修改後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執行完畢後該方法已經有返回值了,後續代碼就不會再執行了),這都是源於異常代碼塊的處理方式,在代碼中try代碼塊就標志著運行時會有一個Throwale線程監視著該方法的運行,若出現異常,則交由異常邏輯處理。

  我們知道方法是在棧內存中運行的,並且會按照“ 先進後出 ”的原則執行,main方法調用了doStuff方法,則main方法在下層,doStuff方法在上層,當doStuff方法執行完" return a " 時,此方法的返回值已經確定int類型1(a變量的值,注意基本類型都是拷貝值,而不是引用),此時finally代碼塊再修改a的值已經與doStuff返回者沒有任何關系了,因此該方法永遠都會返回1.

  繼續追問,那是不是可以在finally代碼塊中修改引用類型的屬性以達到修改返回值的效果呢?代碼如下: 

class Person {
    private String name;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
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)、屏蔽異常

  為什麼明明把異常throw出去了,但main方法卻捕捉不到呢?這是因為異常線程在監視到有異常發生時,就會登記當前的異常類型為DataFormatException,但是當執行器執行finally代碼塊時,則會重新為doStuff方法賦值,也就是告訴調用者" 該方法執行正確,沒有產生異常,返回值為1 ",於是乎,異常神奇的消失了,其簡化代碼如下所示:  

    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方法就不可能獲得任何異常信息了。這樣的代碼會使可讀性大大降低,讀者很難理解作者的意圖,增加了修改的難度。

  在finally中處理return返回值,代碼看上去很完美,都符合邏輯,但是執行起來就會產生邏輯錯誤,最重要的一點是finally是用來做異常的收尾處理的,一旦加上了return語句就會讓程序的復雜度徒然上升,而且會產生一些隱蔽性非常高的錯誤。

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

  注意:不要在finally代碼塊中出現return語句。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved