Effective Java讀書筆記五:異常(57-65)

衣舞晨風發表於2017-02-05

第57條:只針對異常的情況才使用異常

異常是為了在異常情況下使用而設計的,不要將它們用於普通的控制流,也不要編寫迫使它們這麼做的API。

下面部分來自:異常

如果finally塊中出現了異常沒有捕獲或者是捕獲後重新丟擲,則會覆蓋掉try或catch裡丟擲的異常,最終丟擲的異常是finally塊中產生的異常,而不是try或catch塊裡的異常,最後會丟失最原始的異常。

如果在try、catch、finally塊中都丟擲了異常,只是只有一個異常可被傳播到外界。記住,最後被丟擲的異常是唯一被呼叫端接受到的異常,其他異常都被掩蓋而後丟失掉了。如果呼叫端需要知道造成失幾的初始原因,程式之中就絕不能掩蓋任何異常。

請不要在try塊中發出對return、break或continue的呼叫,萬一無法避免,一定要確保finally的存在不會改變函式的返回值(比如說拋異常啊、return啊以及其他任何引起程式退出的呼叫)。因為那樣會引起流程混亂或返回值不確定,如果有返回值最好在try與finally外返回。

不要將try/catch放在迴圈內,那樣會減慢程式碼的執行速度。

如果構造器呼叫的程式碼需要丟擲異常,就不要在構造器處理它,而是直接在構造器宣告上throws出來,這樣更簡潔與安全。因為如果在構造器裡處理異常或將產生異常的程式碼放在構造器之外呼叫,都將會需要呼叫額外的方法來判斷構造的物件是否有效,這樣可能忘記呼叫這些額外的檢查而不安全。

第58條:對可恢復的情況使用受檢異常,對程式設計錯誤使用運用時異常

Java程式設計語言提供了三種異常:受檢的異常(checked exception)、執行時異常(run-time exception)和錯誤(error)。關於什麼時候適合使用哪種異常,雖然沒有明確的規定,但還是有些一般性的原則的。

檢測性異常通常是由外部條件不滿足而引起的,只要條件滿足,程式是可以正常執行的,即可在不修改程式的前提下就可正常執行;而執行時異常則是由於系統內部或程式設計時人為的疏忽而引起的,這種異常一定要修正錯誤程式碼後再能正確執行。受檢異常對客戶是有用的,而執行時異常則是讓開發人員來除錯的,對客戶沒有多大的用處。

在決定使用受檢異常還是未受檢異常時,主要的原則是:如果期望呼叫者能夠適當地恢復,對於這種情況就應該使用受檢的異常。丟擲的受檢異常都是對API使用者的一種潛在的指示:與異常相關的條件是呼叫這個方法的一種可能的結果。

有兩種未受檢的異常:執行時異常和錯誤。在行為上兩種是等同:它們都不需要捕獲。如果丟擲的是未受檢異常或錯誤,往往就屬於不可恢復的情形,繼續執行下去有害無益。如果程式未捕獲這樣的異常或錯誤,將會導致執行緒停止,並出現適當的錯誤訊息。

用執行時異常來表明程式設計錯誤。大多數的執行時異常都表示違返了API規約,API的客戶同有遵守API規範。例如,陣列訪問的約定指明瞭陣列的下標值必須在零和陣列長度減1之間,ArrayIndexOutOfBoundsException表明了這個規定。

按照慣例,錯誤往往被JVM保留用於表示資源不足、約束失敗,或者其他程式無法繼續執行的條件。由於這已經是個幾乎被普遍接受的慣例,因此最好不要再實現任何新的Error子類。因此,你實現的所有未受檢異常都應該是RuntimeException的子類或間接是的。

總而言這,對於可恢復的情況,使用受檢的異常;對於程式錯誤,則使用執行時異常。當然,這也不總是這麼分明的。例如,考慮資源枯竭的情形,這可能是由於程式錯誤而引起的,比如分配了一塊不合理的過大的陣列,也可能確實是由於資源不足而引起的。如果資源枯竭是由於臨時的短缺,或是臨時需求太大所造成的,這種情況可能就是可恢復的。API設計者需要判斷這樣的資源枯竭是否允許。如果你相信可允許恢復,就使用受檢異常,否則使用執行時異常。如果不清楚,最好使用未受檢異常。

因為受檢異常往往指明瞭可恢復的條件,所以,這於這樣的異常,提供一些輔助方法尤其重要,通過這些方法,呼叫都可以獲得一些有助於恢復的資訊。例如,假設因為沒有足夠的錢,他企圖在一個收費電話上呼叫就會失敗,於是丟擲檢查異常。這個異常應該提供一個訪問方法,以便使用者所缺的引用金額,從而可以將這個陣列傳遞給電話使用者。

第59條:避免不必要地使用受檢異常

受檢異常與執行時異常不一樣,它們強迫程式設計師處理異常的條件,大大增強了可靠性,但過分使用受檢異常會使用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);

第60條:優先使用標準異常

常見的可重用異常:

異常 使用時機
IllegalArgumentException 非null的引數值不正確
IllegalStateException 物件狀態不適合方法呼叫
NullPointerException 引數值是null,但這不允許
IndexOutOfBoundsException 索引引數值越界
ConcurrentModificationException 在禁止併發修改的情況下,檢測到物件的併發修改。
UnsupportedOperationException 物件不支援的方法

一定要確保丟擲的異常的條件與該異常的文件中的描述的條件是一致的,如果希望稍微增加更多的失敗-捕獲資訊,可以把現有的異常進行子類化。

第61條:丟擲與抽象物件相對應的異常

如果方法丟擲的異常與所執行的任務沒有明顯的聯絡,這種情形將會使人不知所措,當底層的異常傳播到高層時往往會出現這種情況。這了使人困惑之外,丟擲的底層異常類會汙染高層的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);
}

儘管異常轉換與不加選擇地將捕獲到的底層異常傳播到高層中去相比有所改進,但是它不能濫用。處理來自底層異常的首選做法是根本就讓底層丟擲異常,在呼叫底層方法前確保它會成功,從而來避免丟擲異常,另外,我們有時也可以在呼叫底層方法前,在高層檢查一下引數的有效性,從而也可以避免異常的發生,當然這種做法(不要丟擲底層異常的做法)只是對底層丟擲的是執行時異常時才可行。如果確實無法避免(如低層丟擲的是受檢異常或是執行時異常但根本無法阻止)低層異常時,次選方案是讓高層繞開這些異常,並將異常使用日誌記錄器記錄下來供事後除錯。

總之,處理底層異常最好的方法首選是阻止底層異常的發生,如果不能阻止或者處理底層異常時,一般的做法是使用異常轉換(包括異常鏈轉換),除非底層方法碰巧可以保證丟擲的異常對高層也合適才可以將底層異常直接從底層傳播到高層。異常鏈對高層和低層異常都提供了最佳的功能:它允許丟擲適當的高層異常的同時,又能捕獲底層的原因進行失敗分析。

第62條:每個方法丟擲的異常都要有文件描述

如果一個方法可能丟擲多個異常類,則不要使用“快捷方式”宣告它會丟擲這此異常類的某個超類。永遠不要宣告一個方法“throws Exception”,或者更糟的是宣告“throws Throwable”,這是極端的例子,因為它掩蓋了該方法可能丟擲的其他異常。

對於方法可能丟擲的未受檢異常,如果將這些異常資訊很好地組織成列表文件,就可以有效地描述出這個方法被成功執行的前提條件。每個方法的文件應該描述它的前提條件,這是很重要的,在文件中描述出未受檢的異常是滿中前提條件的最佳做法。

對於掊中的方法,在文件中描述出它可能丟擲的未受檢異常顯得尤其重要。這份文件成了該介面的通用約定的一部分,它指定了該介面的多個實現必須遵循的公共行為。

未受檢異常也要在@throws標籤中進行描述。

如果某類所有方法丟擲同一個異常,那麼這個異常的文件可以描述在類文件中。

總之,要為你編寫的每個方法所能擺好出的每個異常建立文件,對於未受檢和受檢異常,以及對於抽象的和具體的方法也都一樣。

第63條:異常資訊中要包含足夠詳細的異常細節訊息

異常的細節訊息對異常捕獲者非常有用,對異常的診斷是非常有幫助的。

為了捕獲失敗,異常的細節訊息應該包含所有“對該異常有作用”的引數和域值。例如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平臺類庫並沒有使用這種做法,但是,這種做法仍然值得大力推薦。

第64條:努力使失敗保持原子性

當一個物件丟擲一個異常之後,我們總期望這個物件仍然保持在一種定義良好的可用狀態之中。對於被檢查的異常而言,這尤為重要,因為呼叫者通常期望從被檢查的異常中恢復過來。
一般而言,一個失敗的方法呼叫應該保持使物件保持在”它在被呼叫之前的狀態”。具有這種屬性的方法被稱為具有”失敗原子性(failure atomic)”。可以理解為,失敗了還保持著原子性。物件保持”失敗原子性”的方式有幾種:

  • 設計一個非可變物件。
  • 對於在可變物件上執行操作的方法,獲得”失敗原子性”的最常見方法是,在執行操作之前檢查引數的有效性。如下(Stack.java中的pop方法):
public Object pop() {
    if (size==0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null;
    return result;
}
  • 與上一種方法類似,可以對計算處理過程調整順序,使得任何可能會失敗的計算部分都發生在物件狀態被修改之前。
  • 編寫一段恢復程式碼,由它來解釋操作過程中發生的失敗,以及使物件回滾到操作開始之前的狀態上。
  • 在物件的一份臨時拷貝上執行操作,當操作完成之後再把臨時拷貝中的結果複製給原來的物件。

雖然”保持物件的失敗原子性”是期望目標,但它並不總是可以做得到。例如,如果多個執行緒企圖在沒有適當的同步機制的情況下,併發的訪問一個物件,那麼該物件就有可能被留在不一致的狀態中。

即使在可以實現”失敗原子性”的場合,它也不是總被期望的。對於某些操作,它會顯著的增加開銷或者複雜性。
總的規則是:作為方法規範的一部分,任何一個異常都不應該改變物件呼叫該方法之前的狀態,如果這條規則被違反,則API文件中應該清楚的指明物件將會處於什麼樣的狀態。

第65條:不要忽略異常

當一個API的設計者宣告一個方法會丟擲某個異常的時候,他們正在試圖說明某些事情。所以,請不要忽略它!忽略異常的程式碼如下:

try {
    ...
} catch (SomeException e) {
}

空的catch塊會使異常達不到應有的目的,異常的目的是強迫你處理不正常的條件。忽略一個異常,就如同忽略一個火警訊號一樣 – 若把火警訊號器關閉了,那麼當真正的火災發生時,就沒有人看到火警訊號了。所以,catch塊至少應該包含一條說明,用來解釋為什麼忽略這個異常是合適的。

《Effective Java中文版 第2版》PDF版下載:
http://download.csdn.net/detail/xunzaosiyecao/9745699

作者:jiankunking 出處:http://blog.csdn.net/jiankunking

相關文章