《Effective Java》,關於異常

只針對異常的狀況才使用異常

下面展現兩種遍歷數組元素的實現方式。程序員

try {
    int i = 0;
    while(true)
        range[i++].climb();
} catch(ArrayIndexOutOfBoundsException e) {

}
for (Mountain m : range)
    m.climb();

第一種方式在訪問數組邊界以外的第一個數組元素時,用拋出、捕獲、忽略異常的手段來達到終止無限循環的目的。編程

第二種方式是數組循環的標準模式。數組

基於異常的循環模式不只模糊了代碼的意圖,下降了性能,並且還不能保證正常工做。數據結構

異常應該只用於異常的狀況下,不該該用於正常的控制流。
應該優先使用標準的、容易理解的模式,而不是那些聲稱能夠提供更好性能的、弄巧成拙的方法。併發

設計良好的API不該該強迫客戶端爲了正常的控制流而使用異常。
若是類具備「狀態相關」的方法,即只有特定的不可預知的條件下才能夠被調用的方法,這個類每每也應該有個單獨的「狀態測試」方法,即指示是否能夠調用這個狀態相關的方法。
例如,Iterator接口有一個「狀態相關」的next方法,和相應的狀態測試方法hasNext方法。性能

for(Iterator<Foo> i = collection.iterator(); i.hasNext(); ) {
    Foo foo = i.next();
}

若是Iterator缺乏hasNext方法,客戶端將被迫改用下面的作法:學習

try {
    Iterator<Foo> i = collection.iterator();
    while(true) {
        Foo foo = i.next();
    }
} catch (NoSuchElementException e) {

}

對可恢復的狀況使用受檢異常,對編程錯誤使用運行時異常

Java提供了三種可拋出結構:受檢的異常(checked exception)、運行時異常(run-time exception)和錯誤(error)。測試

若是指望調用者可以適當地恢復,使用受檢的異常。this

兩種未受檢的可拋出結構:運行時異常和錯誤。
在行爲上二者是等同的:都是不須要也不該該被捕獲的可拋出結構。atom

用運行時異常代表編程錯誤。大多數的運行時異常都表示前提違例(precondition violation)。前提違例是指API的客戶沒有遵照API規範創建的約定。例如,數組訪問的約定指明瞭數組的下標值必須在零和數組長度減1之間。ArrayIndexOutOfBoundsException代表這個前提被違反了。

最好,全部未受檢的拋出結構都應該是RuntimeException的子類。

避免沒必要要地使用受檢的異常

過度使用受檢的異常會使API使用起來很是不方便。
若是方法拋出一個或者多個受檢的異常,調用該方法的代碼就必須在一個或者多個catch塊中處理這些異常,或者聲明它拋出這些異常,並讓它們傳播出去。

若是正確地使用API並不能阻止這種異常條件的產生,而且一旦產生異常,使用API的程序員能夠當即採起有用的動做,這種負擔就被認爲是正當的。除非這兩個條件都成立,不然更適合於使用受檢的異常。

「把受檢的異常變成未受檢的異常」的一種方法是,把這個拋出異常的方法分紅兩個方法,其中一個方法返回一個boolean,代表是否應該拋出異常。

try {
    obj.action(args);
} catch(TheCheckedException e) {
    // Handle exceptional condition
    ...
}

重構爲:

if (obj.actionPermitted(args)) {
    obj.action(args);
} else {
    // Handle exceptional condition
    ...
}

若是對象在缺乏外部同步的狀況下被併發訪問,或者可被外界改變狀態,這種重構就是不恰當的。由於在actionPermitted和action這兩個調用的時間間隔之中,對象的狀態有可能會發生變化。若是單獨的actionPermitted方法必須重複action方法的工做,出於性能的考慮,這種API重構就不值得去作。

優先使用標準的異常

Java平臺類庫提供了一組基本的未受檢的異常,它們知足了絕大多數API的異常拋出須要。

重用現有的異常有多方面的好處。其中最主要的好處是,使API更加易於學習和使用,由於它與程序員已經熟悉的習慣用法是一致的。第二個好處是,可讀性會更好,由於不會出現不少程序員不熟悉的異常。

經常使用的異常以及其使用場合:
IllegalArgumentException(非null的參數值不正確)
IllegalStateException(對於方法調用而言,對象狀態不合適)
NullPointerException(在禁止使用null的狀況下參數值爲null)
IndexOutOfBoundsException(下標參數值越界)
ConcurrentModificationException(在禁止併發修改的狀況下,檢測到對象的併發修改)
UnsupportedOperationException(對象不支持用戶請求的方法)

選擇重用哪一個異常並不老是那麼精準,由於上表中的「使用場合」並非相互排斥的。

拋出與抽象相對應的異常

若是方法拋出的異常與它所執行的的任務沒有明顯的關係,這種情形將會令人不知所措。

爲了不這個問題,更高層的實現應該捕獲低層的異常,同時拋出能夠按照高層抽象進行解釋的異常。這種作法被稱爲異常轉譯(exception translation)。

取自AbstractSequentialList類的異常轉譯例子:

/**
 * Returns the element at the specified position in this list.
 *
 * <p>This implementation first gets a list iterator pointing to the
 * indexed element (with <tt>listIterator(index)</tt>).  Then, it gets
 * the element using <tt>ListIterator.next</tt> and returns it.
 *
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E get(int index) {
    try {
        return listIterator(index).next();
    } catch (NoSuchElementException exc) {
        throw new IndexOutOfBoundsException("Index: "+index);
    }
}

一種特殊的異常轉譯形式稱爲異常鏈(exception chaining)。若是低層的異常對於調試致使高層異常的問題很是有幫助,使用異常鏈就很合適。

// Exception Chaining
try {
    // use lower-level abstraction to do our bidding
} catch(LowerLevelException cause) {
    throw new HigherLevelException(cause);
}
// Exception with chaining-aware constructor
class HigherLevelException extends Exception {
    HigherLevelException(Throwable cause) {
        super(cause);
    }
}

異常鏈不只讓你能夠經過程序(用getCause)訪問緣由,還能夠將緣由的堆棧軌跡集成到更高層的異常中。

處理來自低層異常的最好作法是,在調用低層方法以前確保它們會成功執行,從而避免它們拋出異常。有時候,在給低層傳遞參數以前,檢查更高層方法的參數的有效性,也能避免低層方法拋出異常。

若是沒法避免低層異常,能夠將異常記錄下來。這樣有助於調查問題,同時又將客戶端代碼和最終用戶與問題隔離開。

總之,若是不能阻止或者處理來自低層的異常,通常作法是使用異常轉譯,除非低層方法碰巧能夠保證它拋出的全部異常對高層也合適,才能夠將異常從低層傳播到高層。異常鏈對高層和低層異常都提供了最佳的功能:容許拋出適當的高層異常,同時又能捕獲低層的緣由進行分析。

每一個方法拋出的異常都要有文檔

始終要單獨地聲明受檢的異常,而且利用Javadoc的@throws標記,準確地記錄下拋出每一個異常的條件。

使用Javadoc的@throws標籤記錄下一個方法可能拋出的每一個未受檢異常,可是不要使用throws關鍵字將未受檢的異常包含在方法的聲明中。

若是一個類中的許多方法出於一樣的緣由而拋出同一個異常,在該類的文檔註釋中對這個異常創建文檔,這是能夠接受的,而不是爲每一個方法單獨創建文檔。

在細節消息中包含能捕獲失敗的信息

當程序因爲未被捕獲的異常而失敗的時候,系統會自動地打印該異常的堆棧軌跡。在堆棧軌跡中包含該異常的字符串表示法,即它的toString方法的調用結果。異常類型的toString方法應該儘量多地返回有關失敗緣由的信息。

爲了捕獲失敗,異常的細節信息應該包含全部「對該異常有貢獻」的參數和域的值。

爲了確保在異常的細節信息中包含足夠的能捕獲失敗的信息,一種辦法是在異常的構造器中引入這些信息。例如:

/**
 * 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;
}

努力使失敗保持原子性

通常而言,失敗的方法調用應該使對象保持在被調用以前的狀態。具備這種屬性的方法被稱爲具備失敗原子性(failure atomic)。

有四種途徑能夠實現這種效果。

第一種是設計一個不可變的對象。

第二種是在執行操做以前檢查參數的有效性。這可使得在對象的狀態被修改以前,先拋出適當的異常。例如,Stack.pop方法。

public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; // Eliminate obsolete reference
    return result;
}

還有相似的辦法是,調整計算處理過程的順序,使得任何可能會失敗的計算部分都在對象狀態被修改以前發生。

第三種是編寫一段恢復代碼,來攔截操做過程當中發生的失敗,以及使對象回滾到操做開始以前的狀態。這種辦法主要用於永久性的數據結構。

第四種是在對象的一份臨時拷貝上執行操做,當操做完成以後再用臨時拷貝中的結果代替對象的內容。

通常,做爲方法規範的一部分,產生的任何異常都應該讓對象保持在該方法調用以前的狀態。

不要忽略異常

當API的設計者聲明一個方法將拋出某個異常的時候,等於正在試圖說明某些事情。因此,請不要忽略它。

有一種情形能夠忽略異常,即關閉FileInputStream的時候。由於尚未改變文件的狀態,所以沒必要執行任何恢復動做,而且已經從文件中讀取到所須要的信息,所以沒必要終止正在進行的操做。即便在這種狀況下,把異常記錄下來仍是明智的作法。

相關文章
相關標籤/搜索