異常是為了在異常情況下使用而設計的,不要將它們用于普通的控制流,也不要編寫迫使它們這么做的API。
下面部分來自:異常
如果finally塊中出現了異常沒有捕獲或者是捕獲后重新拋出,則會覆蓋掉try或catch里拋出的異常,最終拋出的異常是finally塊中產生的異常,而不是try或catch塊里的異常,最后會丟失最原始的異常。
如果在try、catch、finally塊中都拋出了異常,只是只有一個異常可被傳播到外界。記住,最后被拋出的異常是唯一被調用端接受到的異常,其他異常都被掩蓋而后丟失掉了。如果調用端需要知道造成失幾的初始原因,程序之中就絕不能掩蓋任何異常。
請不要在try塊中發出對return、break或continue的調用,萬一無法避免,一定要確保finally的存在不會改變函數的返回值(比如說拋異常啊、return啊以及其他任何引起程序退出的調用)。因為那樣會引起流程混亂或返回值不確定,如果有返回值最好在try與finally外返回。
不要將try/catch放在循環內,那樣會減慢代碼的執行速度。
如果構造器調用的代碼需要拋出異常,就不要在構造器處理它,而是直接在構造器聲明上throws出來,這樣更簡潔與安全。因為如果在構造器里處理異常或將產生異常的代碼放在構造器之外調用,都將會需要調用額外的方法來判斷構造的對象是否有效,這樣可能忘記調用這些額外的檢查而不安全。
java程序設計語言提供了三種異常:受檢的異常(checked exception)、運行時異常(run-time exception)和錯誤(error)。關于什么時候適合使用哪種異常,雖然沒有明確的規定,但還是有些一般性的原則的。
檢測性異常通常是由外部條件不滿足而引起的,只要條件滿足,程序是可以正常運行的,即可在不修改程序的前提下就可正常運行;而運行時異常則是由于系統內部或編程時人為的疏忽而引起的,這種異常一定要修正錯誤代碼后再能正確運行。受檢異常對客戶是有用的,而運行時異常則是讓開發人員來調試的,對客戶沒有多大的用處。
在決定使用受檢異常還是未受檢異常時,主要的原則是:如果期望調用者能夠適當地恢復,對于這種情況就應該使用受檢的異常。拋出的受檢異常都是對API用戶的一種潛在的指示:與異常相關的條件是調用這個方法的一種可能的結果。
有兩種未受檢的異常:運行時異常和錯誤。在行為上兩種是等同:它們都不需要捕獲。如果拋出的是未受檢異常或錯誤,往往就屬于不可恢復的情形,繼續執行下去有害無益。如果程序未捕獲這樣的異常或錯誤,將會導致線程停止,并出現適當的錯誤消息。
用運行時異常來表明編程錯誤。大多數的運行時異常都表示違返了API規約,API的客戶同有遵守API規范。例如,數組訪問的約定指明了數組的下標值必須在零和數組長度減1之間,ArrayIndexOutOfBoundsException表明了這個規定。
按照慣例,錯誤往往被JVM保留用于表示資源不足、約束失敗,或者其他程序無法繼續執行的條件。由于這已經是個幾乎被普遍接受的慣例,因此最好不要再實現任何新的Error子類。因此,你實現的所有未受檢異常都應該是RuntimeException的子類或間接是的。
總而言這,對于可恢復的情況,使用受檢的異常;對于程序錯誤,則使用運行時異常。當然,這也不總是這么分明的。例如,考慮資源枯竭的情形,這可能是由于程序錯誤而引起的,比如分配了一塊不合理的過大的數組,也可能確實是由于資源不足而引起的。如果資源枯竭是由于臨時的短缺,或是臨時需求太大所造成的,這種情況可能就是可恢復的。API設計者需要判斷這樣的資源枯竭是否允許。如果你相信可允許恢復,就使用受檢異常,否則使用運行時異常。如果不清楚,最好使用未受檢異常。
因為受檢異常往往指明了可恢復的條件,所以,這于這樣的異常,提供一些輔助方法尤其重要,通過這些方法,調用都可以獲得一些有助于恢復的信息。例如,假設因為沒有足夠的錢,他企圖在一個收費電話上呼叫就會失敗,于是拋出檢查異常。這個異常應該提供一個訪問方法,以便用戶所缺的引用金額,從而可以將這個數組傳遞給電話用戶。
受檢異常與運行時異常不一樣,它們強迫程序員處理異常的條件,大大增強了可靠性,但過分使用受檢異常會使用API使用起來非常不方便。如果方法拋出一個或者多個受檢異常,調用都就必須在一個或多個catch塊中處理,或者將它們拋出并傳播出去。無論是哪種,都會給程序員添加不可忽視的負擔。
如果方法只拋出單個受檢異常,也會導致該方法不得在try塊中,在這種情況下,應該問自己,是否有別的途徑來避免API調用者使用受檢的異常。這里提供這樣的參考,我們可以把拋出的單個異常的方法分成兩個方法,其中一個方法返回一個boolean,表明是否該拋出異常。這種API重構,把下面的調用:
try{//調用時檢查異常 obj.action(args);//調用檢查異常方法}catch(TheCheckedExcption e){ // 處理異常條件 ...}重構為:
if(obj.actionPermitted(args)){//使用狀態測試方法消除catch obj.action(args);}else{ // 處理異常條件 ...}這種重構并不總是合適的,但在合適的地方,它會使用API用起來更加舒服。雖然沒有前者漂亮,但更加靈活——如果程序員知道調用肯定會成功,或不介意由調用失敗而導致的線程終止,則下面為理為簡單的調用形式:
obj.action(args);常見的可重用異常:
異常 | 使用時機 |
---|---|
IllegalArgumentException | 非null的參數值不正確 |
IllegalStateException | 對象狀態不適合方法調用 |
NullPointerException | 參數值是null,但這不允許 |
IndexOutOfBoundsException | 索引參數值越界 |
ConcurrentModificationException | 在禁止并發修改的情況下,檢測到對象的并發修改。 |
UnsupportedOperationException | 對象不支持的方法 |
一定要確保拋出的異常的條件與該異常的文檔中的描述的條件是一致的,如果希望稍微增加更多的失敗-捕獲信息,可以把現有的異常進行子類化。
如果方法拋出的異常與所執行的任務沒有明顯的聯系,這種情形將會使人不知所措,當底層的異常傳播到高層時往往會出現這種情況。這了使人困惑之外,拋出的底層異常類會污染高層的API(高層要依賴于底層異常類)。為了避免這個問題,高層在捕獲底層拋出的異常的同時,在捕獲的地方將底層的異常轉換后再重新拋出會更好:
// 異常轉換try {// 調用底層方法...} catch(LowerLevelException e) { //捕獲底層拋出的異常后并轉換成適合自己系統的異常后再重新拋出throw new HigherLevelException(...);}下面是個來自AbstractSequentialList類中的底層異常轉換的實例,該數是List的一個抽象類,它的直接子類為LinkedList,在這個例子中,按照List接口中的get方法的規范(規范中說到:如果索引超出范圍 (index < 0 || index >= size()),就會拋出IndexOutOfBoundsException異常),底層方法只要可能拋出異常,我們就需要轉換這個異常,下面是AbstractSequentialList類庫的做法:
/*** Returns the element at the specified position in this list.* @throws IndexOutOfBoundsException if the index is out of range* ({@code index < 0 || index >= size()}).*/public E get(int index) {ListIterator<E> i = listIterator(index);try {return i.next();//Iterator的next會拋出NoSuchElementException運行時異常} catch(NoSuchElementException e) {/** 但接口規范是要求拋出IndexOutOfBoundsException異常,所以需要轉換。當然這種* 轉換也是合理的,因為該方法的功能特性就是按索引來取元素,在索引越界的情況* 下拋出NoSuchElementException也是沒有太大的問題的(當然劈開規范來說的),但* 拋IndexOutOfBoundsException異常會更適合一些*/throw new IndexOutOfBoundsException("Index: " + index);}}另一種異常轉換的形式是異常鏈,如果底層的異常對于高層調試有很大幫助時,使用異常鏈就非常合適,這樣在高層我們可以通過相應的方法來獲取底層拋出的異常:
// 異常鏈try {... // 調用底層方法} catch (LowerLevelException cause) { // 構造異常鏈后重新拋出throw new HigherLevelException(cause);}盡管異常轉換與不加選擇地將捕獲到的底層異常傳播到高層中去相比有所改進,但是它不能濫用。處理來自底層異常的首選做法是根本就讓底層拋出異常,在調用底層方法前確保它會成功,從而來避免拋出異常,另外,我們有時也可以在調用底層方法前,在高層檢查一下參數的有效性,從而也可以避免異常的發生,當然這種做法(不要拋出底層異常的做法)只是對底層拋出的是運行時異常時才可行。如果確實無法避免(如低層拋出的是受檢異常或是運行時異常但根本無法阻止)低層異常時,次選方案是讓高層繞開這些異常,并將異常使用日志記錄器記錄下來供事后調試。
總之,處理底層異常最好的方法首選是阻止底層異常的發生,如果不能阻止或者處理底層異常時,一般的做法是使用異常轉換(包括異常鏈轉換),除非底層方法碰巧可以保證拋出的異常對高層也合適才可以將底層異常直接從底層傳播到高層。異常鏈對高層和低層異常都提供了最佳的功能:它允許拋出適當的高層異常的同時,又能捕獲底層的原因進行失敗分析。
如果一個方法可能拋出多個異常類,則不要使用“快捷方式”聲明它會拋出這此異常類的某個超類。永遠不要聲明一個方法“throws Exception”,或者更糟的是聲明“throws Throwable”,這是極端的例子,因為它掩蓋了該方法可能拋出的其他異常。
對于方法可能拋出的未受檢異常,如果將這些異常信息很好地組織成列表文檔,就可以有效地描述出這個方法被成功執行的前提條件。每個方法的文檔應該描述它的前提條件,這是很重要的,在文檔中描述出未受檢的異常是滿中前提條件的最佳做法。
對于掊中的方法,在文檔中描述出它可能拋出的未受檢異常顯得尤其重要。這份文檔成了該接口的通用約定的一部分,它指定了該接口的多個實現必須遵循的公共行為。
未受檢異常也要在@throws標簽中進行描述。
如果某類所有方法拋出同一個異常,那么這個異常的文檔可以描述在類文檔中。
總之,要為你編寫的每個方法所能擺好出的每個異常建立文檔,對于未受檢和受檢異常,以及對于抽象的和具體的方法也都一樣。
異常的細節消息對異常捕獲者非常有用,對異常的診斷是非常有幫助的。
為了捕獲失敗,異常的細節消息應該包含所有“對該異常有作用”的參數和域值。例如IndexOutOfBoundsException異常的細節消息應該包含下界、上界以及沒有落在界內的下標值,因為這三個值都有可能引起這個異常。
異常的細節消息不應該與“用戶層次的錯誤消息”混為一談,后都對于最終用戶而言必須是可理解的。與用戶層次的錯誤消息不同,異常的詳細消息主要是讓程序員用來分析失敗原因的。因此,異常細節消息的內容比可理解性重要得多。
為了確保在異常的細節消息中包含足夠的能捕獲失敗的信息,一種辦法是在異常的構造器而不是字符串細節消息中引入這些信息。然后,有了這些信息,只要把它們放到消息描述中,就可以自動產生細節消息。例如,IndexOutOfBoundsException本應該這樣設計的:
/*** Construct an IndexOutOfBoundsException.** @param lowerBound the lowest legal index value.* @param upperBound the highest legal index value plus one.* @param index the actual index value.*/public IndexOutOfBoundsException(int lowerBound, int upperBound,int index) {// 構建詳細的捕獲消息super("Lower bound: " + lowerBound +", Upper bound: " + upperBound +", Index: " + index);// 存儲失敗的細節消息供程序訪問this.lowerBound = lowerBound;this.upperBound = upperBound;this.index = index;}但遺憾的是,Java平臺類庫并沒有使用這種做法,但是,這種做法仍然值得大力推薦。
當一個對象拋出一個異常之后,我們總期望這個對象仍然保持在一種定義良好的可用狀態之中。對于被檢查的異常而言,這尤為重要,因為調用者通常期望從被檢查的異常中恢復過來。 一般而言,一個失敗的方法調用應該保持使對象保持在”它在被調用之前的狀態”。具有這種屬性的方法被稱為具有”失敗原子性(failure atomic)”。可以理解為,失敗了還保持著原子性。對象保持”失敗原子性”的方式有幾種:
設計一個非可變對象。對于在可變對象上執行操作的方法,獲得”失敗原子性”的最常見方法是,在執行操作之前檢查參數的有效性。如下(Stack.java中的pop方法):public Object pop() { if (size==0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; return result;}與上一種方法類似,可以對計算處理過程調整順序,使得任何可能會失敗的計算部分都發生在對象狀態被修改之前。 編寫一段恢復代碼,由它來解釋操作過程中發生的失敗,以及使對象回滾到操作開始之前的狀態上。在對象的一份臨時拷貝上執行操作,當操作完成之后再把臨時拷貝中的結果復制給原來的對象。雖然”保持對象的失敗原子性”是期望目標,但它并不總是可以做得到。例如,如果多個線程企圖在沒有適當的同步機制的情況下,并發的訪問一個對象,那么該對象就有可能被留在不一致的狀態中。
即使在可以實現”失敗原子性”的場合,它也不是總被期望的。對于某些操作,它會顯著的增加開銷或者復雜性。 總的規則是:作為方法規范的一部分,任何一個異常都不應該改變對象調用該方法之前的狀態,如果這條規則被違反,則API文檔中應該清楚的指明對象將會處于什么樣的狀態。
當一個API的設計者聲明一個方法會拋出某個異常的時候,他們正在試圖說明某些事情。所以,請不要忽略它!忽略異常的代碼如下:
try { ...} catch (SomeException e) {}空的catch塊會使異常達不到應有的目的,異常的目的是強迫你處理不正常的條件。忽略一個異常,就如同忽略一個火警信號一樣 – 若把火警信號器關閉了,那么當真正的火災發生時,就沒有人看到火警信號了。所以,catch塊至少應該包含一條說明,用來解釋為什么忽略這個異常是合適的。
《Effective Java中文版 第2版》PDF版下載: http://download.csdn.net/detail/xunzaosiyecao/9745699
作者:jiankunking 出處:http://blog.csdn.net/jiankunking
新聞熱點
疑難解答