讀古今文學網 > 編寫高質量代碼:改善Java程序的151個建議 > 第8章 異常 >

第8章 異常

大成若缺,其用不弊。

大盈若沖,其用不窮。

——老子《道德經》

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

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

建議110:提倡異常封裝

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

(1)提高系統的友好性

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


public static void doStuff()throws Exception{

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(e);

}

}


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

來看如下代碼:


public void doStuff(){

try{

//do something

}catch(Exception e){

e.printStackTrace();

}

}


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

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


public void doStuff(){

try{

//do something

}catch(FileNotFoundException e){

log.info(\"文件未找到,使用默認配置文件……\");

}catch(SecurityException e){

log.error(\"無權訪問,可能原因是……\");

e.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 static 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對像進行校驗,然後返回所有的異常。