大成若缺,其用不弊。
大盈若沖,其用不窮。
——老子《道德經》
不管人類的思維有多麼縝密,也存在「智者千慮必有一失」的缺憾。無論計算機技術怎麼發展,也不可能窮盡所有的情景——這個世界是不完美的,是有缺陷的,完美的世界只存在於理想中。
對於軟件帝國的締造者來說,程序也是不完美的,異常情況隨時都會出現,我們需要它為我們描述例外事件,需要它處理非預期的情景,需要它幫我們建立「完美世界」。
建議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對像進行校驗,然後返回所有的異常。