無論人類的思惟有多麼縝密,也存在" 智者千慮必有一失 "的缺憾。不管計算機技術怎麼發展,也不可能窮盡全部的場景___這個世界是不完美的,也是有缺陷的。完美的世界只存在於理想中。面試
對於軟件帝國的締造者來講,程序也是不完美的,異常狀況會隨時出現,咱們須要它爲咱們描述例外事件,須要它處理非預期的情景,須要它幫咱們創建「完美世界」。數據庫
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)、提升系統的可維護性this
看以下代碼: 編碼
public void doStuff3(){ try{ //doSomething }catch(Exception e){ e.printStackTrace(); } }
這是你們很容易犯的錯誤,拋出異常是吧?分類處理多麻煩,就寫一個catch塊來處理全部的異常吧,並且還信誓旦旦的說" JVM會打印出棧中的錯誤信息 ",雖然這沒錯,可是該信息只有開發人員本身看的懂,維護人員看到這段異常時基本上沒法處理,由於須要到代碼邏輯中去分析問題。spa
正確的作法是對異常進行分類處理,並進行封裝輸出,代碼以下:
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對象進行校驗,而後返回全部的異常。
設計模式中有一個模式叫作責任鏈模式(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的異常信息,如此便可方便地查找到產生異常的基本信息,便於解決問題。
結合上一建議來講,異常須要封裝和傳遞,咱們在進行系統開發時不要" 吞噬 " 異常,也不要赤裸裸的拋出異常,封裝後再拋出,或者經過異常鏈傳遞,能夠達到系統更健壯,更友好的目的。
爲何說是" 儘量"的轉化呢?由於" 把全部的受檢異常(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異常問題的處理。
這裏產生了兩個問題:1、 異常是主邏輯的補充邏輯,修改一個補充邏輯,就會致使主邏輯也被修改,也就是出現了實現類 " 逆影響 " 接口的情景,咱們知道實現類是不穩定的,而接口是穩定的,一旦定義了異常,則增長了接口的不穩定性,這是面向對象設計的嚴重褻瀆;2、實現的變動最終會影響到調用者,破壞了封裝性,這也是迪米特法則所不能容忍的。
(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接口爲例,咱們在聲明接口時再也不聲明異常,而是在具體實現時根據不一樣的狀況產生不一樣的非受檢異常,這樣持久層和邏輯層拋出的異常將會由展示自行決定如何展現,再也不受異常的規則約束了,大大簡化開發工做,提升了代碼的可讀性。
那問題又來了,在開發和設計時什麼樣的受檢異常有必要化爲非受檢異常呢?" 儘量 " 是以什麼做爲判斷依據呢?受檢異常轉換爲非受檢異常是須要根據項目的場景來決定的,例如一樣是刷卡,員工拿着本身的工卡到考勤機上打考勤,此時若是附近有磁性物質干擾,則考勤機能夠把這種受檢異常轉化爲非受檢異常,黃燈閃爍後不作任何記錄登記,由於考勤失敗這種情景不是" 致命 "的業務邏輯,出錯了,從新刷一下便可。可是到銀行網點取錢就不同了,拿着銀行卡到銀行取錢,一樣有磁性物質干擾,刷不出來,那這種異常就必須登記處理,不然會成爲威脅銀行卡安全的事件。彙總成一句話:當受檢異常威脅到了系統的安全性,穩定性,可靠性、正確性時,則必須處理,不能轉化爲非受檢異常,其它狀況則能夠轉化爲非受檢異常。
注意:受檢異常威脅到系統的安全性,穩定性、可靠性、正確性時,不能轉換爲非受檢異常。
在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語句。