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

建議114:不要在構造函數中拋出異常

  Java異常的機制有三種:程序員

  • Error類及其子類表示的是錯誤,它是不須要程序員處理也不能處理的異常,好比VirtualMachineError虛擬機錯誤,ThreadDeath線程僵死等。
  • RunTimeException類及其子類表示的是非受檢異常,是系統可能會拋出的異常,程序員能夠去處理,也能夠不處理,最經典的就是NullPointException空指針異常和IndexOutOfBoundsException越界異常。
  • Exception類及其子類(不包含非受檢異常),表示的是受檢異常,這是程序員必須處理的異常,不處理則程序不能經過編譯,好比IOException表示的是I/O異常,SQLException表示的數據庫訪問異常。  

咱們知道,一個對象的建立過程通過內存分配,靜態代碼初始化、構造函數執行等過程,對象生成的關鍵步驟是構造函數,那是否是也容許在構造函數中拋出異常呢?從Java語法上來講,徹底能夠在構造函數中拋出異常,三類異常均可以,可是從系統設計和開發的角度來分析,則儘可能不要在構造函數中拋出異常,咱們以三種不一樣類型的異常來講明之。數據庫

(1)、構造函數中拋出錯誤是程序員沒法處理的編程

  在構造函數執行時,若發生了VirtualMachineError虛擬機錯誤,那就沒招了,只能拋出,程序員不能預知此類錯誤的發生,也就不能捕捉處理。緩存

(2)、構造函數不該該拋出非受檢異常ide

  咱們來看這樣一個例子,代碼以下:函數

class Person {
    public Person(int _age) {
        // 不滿18歲的用戶對象不能創建
        if (_age < 18) {
            throw new RuntimeException("年齡必須大於18歲.");
        }
    }

    public void doSomething() {
        System.out.println("doSomething......");
    }
}

  這段代碼的意圖很明顯,年齡不滿18歲的用戶不會生成一個Person實例對象,沒有對象,類行爲doSomething方法就不可執行,想法很好,但這會致使不可預測的結果,好比咱們這樣引用Person類: 性能

public static void main(String[] args) {
        Person p =  new Person(17);
        p.doSomething();
        /*其它的業務邏輯*/
    }

  很顯然,p對象不能創建,由於是一個RunTimeException異常,開發人員能夠捕捉也能夠不捕捉,代碼看上去邏輯很正確,沒有任何瑕疵,可是事實上,這段程序會拋出異常,沒法執行。這段代碼給了咱們兩個警示:測試

  1. 加劇了上層代碼編寫者的負擔:捕捉這個RuntimeException異常吧,那誰來告訴我有這個異常呢?只有經過文檔約束了,一旦Person類的構造函數通過重構後再拋出其它非受檢異常,那main方法不用修改也是能夠測試經過的,可是這裏就可能會產生隱藏的缺陷,而寫仍是很難重現的缺陷。不捕捉這個RuntimeException異常,這個是咱們一般的想法,既然已經寫成了非受檢異常,main方法的編碼者徹底能夠不處理這個異常嘛,大不了不執行Person的方法!這是很是危險的,一旦產生異常,整個線程都再也不繼續執行,或者連接沒有關閉,或者數據沒有寫入數據庫,或者產生內存異常,這些都是會對整個系統產生影響。
  2. 後續代碼不會執行:main方法的實現者本來是想把p對象的創建做爲其代碼邏輯的一部分,執行完doSomething方法後還須要完成其它邏輯,可是由於沒有對非受檢異常進行捕捉,異常最終會拋出到JVM中,這會致使整個線程執行結束後,後面全部的代碼都不會繼續執行了,這就對業務邏輯產生了致命的影響。

(3)、構造函數儘量不要拋出受檢異常優化

  咱們來看下面的例子,代碼以下:this

//父類
class Base {
    // 父類拋出IOException
    public Base() throws IOException {
        throw new IOException();
    }
}
//子類
class Sub extends Base {
    // 子類拋出Exception異常
    public Sub() throws Exception {

    }
}

  就這麼一段簡單的代碼,展現了在構造函數中拋出受檢異常的三個不利方面:

  1. 致使子類膨脹:在咱們的例子中子類的無參構造函數不能省略,緣由是父類的無參構造函數拋出了IOException異常,子類的無參構造函數默認調用的是父類的構造函數,因此子類無參構造函數也必須拋出IOException或其父類。
  2. 違背了里氏替換原則"里氏替換原則" 是說父類能出現的地方子類就能夠出現,並且將父類替換爲子類也不會產生任何異常。那咱們回頭看看Sub類是否能夠替換Base類,好比咱們的上層代碼是這樣寫的:
public static void main(String[] args) {
        try {
            Base base = new Base();
        } catch (Exception e) {    
            e.printStackTrace();
        }
    }

  而後,咱們指望把new Base()替換成new Sub(),並且代碼可以正常編譯和運行。很是惋惜,編譯不經過,緣由是Sub的構造函數拋出了Exception異常,它比父類的構造函數拋出更多的異常範圍要寬,必須增長新的catch塊才能解決。  

  可能你們要問了,爲何Java的構造函數容許子類的構造函數拋出更普遍的異常類呢?這正好與類方法的異常機制相反,類方法的異常是這樣要求的:

// 父類
class Base {
    // 父類方法拋出Exception
    public void testMethod() throws Exception {

    }
}

// 子類
class Sub extends Base {
    // 父類方法拋出Exception
    @Override
    public void testMethod() throws IOException {

    }
}

  子類的方法能夠拋出多個異常,但都必須是覆寫方法的子類型,對咱們的例子來講,Sub類的testMethod方法拋出的異常必須是Exception的子類或Exception類,這是Java覆寫的要求。構造函數之因此於此相反,是由於構造函數沒有覆寫的概念,只是構造函數間的引用調用而已,因此在構造函數中拋出受檢異常會違背里氏替換原則原則,使咱們的程序缺少靈活性。

  3.子類構造函數擴展受限:子類存在的緣由就是指望實現擴展父類的邏輯,但父類構造函數拋出異常卻會讓子類構造函數的靈活性大大下降,例如咱們指望這樣的構造函數。

// 父類
class Base {
    public Base() throws IOException{
        
    }
}
// 子類
class Sub extends Base {
    public Sub() throws Exception{
        try{
            super();
        }catch(IOException e){
            //異常處理後再拋出
            throw e;
        }finally{
            //收尾處理
        }
    }
}

  很不幸,這段代碼編譯不經過,緣由是構造函數Sub沒有把super()放在第一句話中,想把父類的異常從新包裝再拋出是不可行的(固然,這裏有不少種 「曲線」 的實現手段,好比從新定義一個方法,而後父子類的構造函數都調用該方法,那麼子類構造函數就能夠自由處理異常了),這是Java語法機制。

  將以上三種異常類型彙總起來,對於構造函數,錯誤只能拋出,這是程序人員無能爲力的事情;非受檢異常不要拋出,拋出了 " 對己對人 " 都是有害的;受檢異常儘可能不拋出,能用曲線的方式實現就用曲線方式實現,總之一句話:在構造函數中儘量不出現異常。

  注意 :在構造函數中不要拋出異常,儘可能曲線實現。

建議115:使用Throwable得到棧信息

  AOP編程能夠很輕鬆的控制一個方法調用哪些類,也可以控制哪些方法容許被調用,通常來講切面編程(好比AspectJ),只能控制到方法級別,不能實現代碼級別的植入(Weave),好比一個方法被類A的m1方法調用時返回1,在類B的m2方法調用時返回0(同參數狀況下),這就要求被調用者具備識別調用者的能力。在這種狀況下,可使用Throwable得到棧信息,而後鑑別調用者並分別輸出,代碼以下: 

class Foo {
    public static boolean method() {
        // 取得當前棧信息
        StackTraceElement[] sts = new Throwable().getStackTrace();
        // 檢查是不是methodA方法調用
        for (StackTraceElement st : sts) {
            if (st.getMethodName().equals("methodA")) {
                return true;
            }
        }
        return false;
    }
}
//調用者
class Invoker{
    //該方法打印出true
    public static void methodA(){
        System.out.println(Foo.method());
    }
    //該方法打印出false
    public static void methodB(){
        System.out.println(Foo.method());
    }
}

  注意看Invoker類,兩個方法methodAmethodB都調用了Foo的method方法,都是無參調用,返回值卻不一樣,這是咱們的Throwable類發揮效能了。JVM在建立一本Throwable類及其子類時會把當前線程的棧信息記錄下來,以便在輸出異常時準肯定位異常緣由,咱們來看Throwable源代碼。

public class Throwable implements Serializable {
    private static final StackTraceElement[] UNASSIGNED_STACK = new StackTraceElement[0];
    //出現異常記錄的棧幀
    private StackTraceElement[] stackTrace = UNASSIGNED_STACK;
    //默認構造函數
    public Throwable() {
//記錄棧幀 fillInStackTrace(); } //本地方法,抓取執行時的棧信息
private native Throwable fillInStackTrace(int dummy); public synchronized Throwable fillInStackTrace() { if (stackTrace != null || backtrace != null /* Out of protocol state */) { fillInStackTrace(0); stackTrace = UNASSIGNED_STACK; } return this; } }

  在出現異常時(或主動聲明一個Throwable對象時),JVM會經過fillInStackTrace方法記錄下棧幀信息,而後生成一個Throwable對象,這樣咱們就能夠知道類間的調用順序,方法名稱及當前行號等了。

  得到棧信息能夠對調用者進行判斷,而後決定不一樣的輸出,好比咱們的methodA和methodB方法,一樣地輸入參數,一樣的調用方法,可是輸出卻不一樣,這看起來很想是一個bug:方法methodA調用method方法正常顯示,而方法methodB調用卻會返回錯誤數據,所以咱們雖然能夠根據調用者的不一樣產生不一樣的邏輯,但這僅侷限在對此方法的普遍認知上,更多的時候咱們使用method方法的變形體,代碼以下:  

class Foo {
    public static boolean method() {
        // 取得當前棧信息
        StackTraceElement[] sts = new Throwable().getStackTrace();
        // 檢查是不是methodA方法調用
        for (StackTraceElement st : sts) {
            if (st.getMethodName().equals("methodA")) {
                return true;
            }
        }
        throw new RuntimeException("除了methodA方法外,該方法不容許其它方法調用");
    }
}

  只是把「return false」 替換成了一個運行期異常,除了methodA方法外,其它方法調用都會產生異常,該方法經常使用做離線註冊碼校驗,讓破解者視圖暴力破解時,因爲執行者不是指望的值,所以會返回一個通過包裝和混淆的異常信息,大大增長了破解難度。

建議116:異常只爲異常服務

  異常只爲異常服務,這是何解?難道異常還能爲其它服務不成?確實能,異常本來是正常邏輯的一個補充,可是有時候會被當作主邏輯使用,看以下代碼:

//判斷一個枚舉是否包含String枚舉項
    public static <T extends Enum<T>> boolean Contain(Class<T> clz,String name){
        boolean result = false;
        try{
            Enum.valueOf(clz, name);
            result = true;
        }catch(RuntimeException e){
            //只要是拋出異常,則認爲不包含
        }
        return result;
    }

  判斷一個枚舉是否包含指定的枚舉項,這裏會根據valueOf方法是否拋出異常來進行判斷,若是拋出異常(通常是IllegalArgumentException異常),則認爲是不包含,若不拋出異常則能夠認爲包含該枚舉項,看上去這段代碼很正常,可是其中有是哪一個錯誤:

  1. 異常判斷下降了系統的性能
  2. 下降了代碼的可讀性,只有詳細瞭解valueOf方法的人才能讀懂這樣的代碼,由於valueOf拋出的是一個非受檢異常
  3. 隱藏了運行期可能產生的錯誤,catch到異常,但沒有作任何處理。

  咱們這段代碼是用一段異常實現了一個正常的業務邏輯,這致使代碼產生了壞味道。要解決從問題也很容易,即不在主邏輯中實使用異常,代碼以下:  

    // 判斷一個枚舉是否包含String枚舉項
    public static <T extends Enum<T>> boolean Contain(Class<T> clz, String name) {
        // 遍歷枚舉項
        for (T t : clz.getEnumConstants()) {
            // 枚舉項名稱是否相等
            if (t.name().equals(name)) {
                return true;
            }
        }
        return false;
    }

  異常只能用在非正常的狀況下,不能成爲正常狀況下的主邏輯,也就是說,異常是是主邏輯的輔助場景,不能喧賓奪主。

  並且,異常雖然是描述例外事件的,但能避免則避免之,除非是確實沒法避免的異常,例如: 

public static void main(String[] args) {
        File file = new File("a.txt");
        try {
            FileInputStream fis = new FileInputStream(file);
            // 其它業務處理
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            // 異常處理
        }
    }

  這樣一段代碼常常在咱們的項目中出現,但常常寫並不表明不可優化,這裏的異常類FileNotFoundException徹底能夠在它誕生前就消除掉:先判斷文件是否存在,而後再生成FileInputStream對象,這也是項目中常見的代碼:

    public static void main(String[] args) {
        File file = new File("a.txt");
        // 常常出現的異常,能夠先作判斷
        if (file.exists() && !file.isDirectory()) {
            try {
                FileInputStream fis = new FileInputStream(file);
                // 其它業務處理
            } catch (FileNotFoundException e) {
                e.printStackTrace();
                // 異常處理
            }
        }
    }

  雖然增長了if判斷語句,增長了代碼量,可是卻減小了FileNotFoundException異常出現的概率,提升了程序的性能和穩定性。

建議117:多使用異常,把性能問題放一邊

  咱們知道異常是主邏輯的例外邏輯,舉個簡單的例子來講,好比我在馬路上走(這是主邏輯),忽然開過一輛車,我要避讓(這是受檢異常,必須處理),繼續走着,忽然一架飛機從我頭頂飛過(非受檢異常),咱們能夠選在繼續行走(不捕捉),也能夠選擇指責其噪音污染(捕捉,主邏輯的補充處理),再繼續走着,忽然一顆流星砸下來,這沒有選擇,屬於錯誤,不能作任何處理。這樣具有完整例外場景的邏輯就具有了OO的味道,任何一個事務的處理均可能產生非預期的效果,問題是須要以何種手段來處理,若是不使用異常就須要依靠返回值的不一樣來進行處理了,這嚴重失去了面向對象的風格。

  咱們在編寫用例文檔(User case Specification)時,其中有一項叫作 " 例外事件 ",是用來描述主場景外的例外場景的,例如用戶登陸的用例,就會在" 例外事件 "中說明" 連續3此登陸失敗即鎖定用戶帳號 ",這就是登陸事件的一個異常處理,具體到咱們的程序中就是:  

public void login(){
        try{
            //正常登錄
        }catch(InvalidLoginException lie){
            //    用戶名無效
        }catch(InvalidPasswordException pe){
            //密碼錯誤的異常
        }catch(TooMuchLoginException){
            //屢次登錄失敗的異常
        }
    }

  如此設計則可讓咱們的login方法更符合實際的處理邏輯,同時使主邏輯(正常登陸,try代碼塊)更加清晰。固然了,使用異常還有不少優勢,可讓正常代碼和異常代碼分離、能快速查找問題(棧信息快照)等,可是異常有一個缺點:性能比較慢。

  Java的異常機制確實比較慢,這個"比較慢"是相對於諸如String、Integer等對象來講的,單單從對象的建立上來講,new一個IOException會比String慢5倍,這從異常的處理機制上也能夠解釋:由於它要執行fillInStackTrace方法,要記錄當前棧的快照,而String類則是直接申請一個內存建立對象,異常類慢一籌也就在所不免了。

  並且,異常類是不能緩存的,指望先創建大量的異常對象以提升異常性能也是不現實的。

  難道異常的性能問題就沒有任何能夠提升的辦法了?確實沒有,可是咱們不能由於性能問題而放棄使用異常,並且通過測試,在JDK1.6下,一個異常對象的建立時間只需1.4毫秒左右(注意是毫秒,一般一個交易是在100毫秒左右),難道咱們的系統連如此微小的性能消耗都不予許嗎?

 注意:性能問題不是拒絕異常的藉口。

相關文章
相關標籤/搜索