Java的基本理念是,「結構不佳的代碼不能運行」java
—— Java編程思想程序員
發現錯誤的理想時機是在編譯階段,也就是試圖運行程序以前。然而,編譯階段並不能找出全部的錯誤,餘下的問題必須在運行期間解決。這就須要錯誤源可以經過某種方式,把適當的信息傳遞給某個接收者——該接收者將知道如何正確處理這個問題。編程
舉個例子,當一個方法遇到一種狀況,這種狀況下,它不能知足約定,這時應該如何處理?數組
傳統的作法是方法應該返回某種錯誤碼。調用者被迫區檢查錯誤,若是調用者也不能處理錯誤,那就給調用者的調用者返回一個錯誤碼……架構
程序員並不老是檢查和傳遞返回的錯誤碼,結果錯誤沒有被檢測到,致使後面的嚴重破壞!併發
C以及其餘早期語言經常具備多種錯誤處理模式,這些模式每每創建在約定俗成的基礎之上,而並不屬於語言的一部分。ide
從一個反面教材開始:測試
try { int i = 0; while (true) range[i++].climb(); } catch (ArrayIndexOutOfBoundsException e) { }
上面這種狀況就是亂用異常的典型,它的構想是很是拙劣的。當這個循環企圖訪問數組邊界以外的第一個數組元素時,用throw、catch和忽略ArrayIndexOutOfBoundsException的手段來達到終止無限循環的目的,對於任何一個合格的程序員來講,它的實現用下面的標準模式一看便知:優化
for (Mountain m : range) m.climb();
異常機制的設計初衷是用於不正常的情形。因此不多會有JVM實現試圖對它們進行優化,使得異常與顯示的測試同樣快速,例如for等。把代碼放到try-catch塊中,反而阻止了JVM實現原本可能要執行的某些特定優化。這個例子說明:atom
異常應該只用於異常的狀況下,他們永遠不該該用於正常的控制流;
設計良好的API不該該強迫它爲了正常的控制流而使用異常;
全部的異常類都派生自Throwable。當發生來某種異常,而這種異常不是指望應用程序處理的,好比內存耗盡等,則JVM會拋出Error。這種異常不推薦程序員使用。
通常程序員使用的異常屬於Exception類的異常,他們分爲兩種:
未檢查異常(unchecked exception),屬於RuntimeException的子類,它屬於不須要也不該該被捕獲的可拋出結構。用運行時異常來代表編程錯誤。大多數的運行時異常都表示前提違例(precondition violation),意思是API的客戶沒有遵照API的規範創建的約定,好比要求參數不能爲Null,但仍是傳遞Null參數,實現的全部未檢查異常(未受檢異常)都應該是RuntimeException的子類;
已檢查異常(checked exception),屬於Exception的子類,也稱爲受檢異常,若是指望調用者可以適當地恢復,對於這種狀況就應該使用它。經過拋出異常,強迫調用者在一個catch裏處理該異常,或將它傳播出去,尤爲在設計API時,設計者讓API用戶面對受檢的異常;
異常也是個對象,能夠在它上面定義任意的方法。這些方法的主要用途是爲捕獲異常的代碼提供額外的信息,特別是關於引起這個異常條件的信息。
受檢異常強迫使用該API的用戶必須catch異常,大大增長來程序的可靠性。可是多度使用會使API使用起來很是不方便。用戶必須聲明它拋出的這些異常,或者讓它們傳播出去,不管哪一種方法,都給API用戶增添來負擔。
那何時使用受檢異常比較合適呢?
若是,正確的使用了API,但並不能組織這種異常條件的發生,而且一旦產生異常,使用API的用戶會當即採起有用的動做,這種狀況就是使用受檢異常的典型場景。
例如,API的設計人員能夠嘗試着問本身:API用戶將如何處理該異常,會有比下面代碼更好的方式嗎?
catch (CheckedException e) { throw new AssertionError(); }
下面這種作法如何?
catch (CheckedException e) { e.printStackTrace(); System.exit(1); }
若是使用API的用戶沒法作的比這更好,那仍是使用未受檢異常更爲合適。
這段代碼的意思爲,若是這個異常發生了,程序註定會中止,或者要完成的事情註定不能夠完成,那直接拋出未受檢異常讓程序中斷,暴露出問題更爲合適。
例如,在繳費電話費時,因爲餘額不足致使繳費失敗,能夠拋出受檢異常,並提供一個方法,查詢所需的金額等。
代碼重用是值得提倡的,這是一條通用的規則,異常也不例外。
Java提供來一組基本的未受檢異常,它們知足了絕大多數API的異常需求。
異常 | 場合 |
IllegalArgumentException | 非null的參數值不正確 |
IllegalStateException | 對於方法調用而言,對象狀態不合適 |
NullPointerException | 在禁止使用null的狀況下參數值爲null |
IndexOutOfBoundsException | 下標參數值越界 |
ConcurrentModificationException | 進行併發修改的狀況下,檢測到併發修改對象 |
UnsupportedOperationException | 對象不支持用戶請求的方法 |
若是方法拋出的異常與它所執行的任務沒有明顯聯繫,這種情形將會令人不知所措。
更高層的實現應該捕獲底層的異常,同時拋出能夠按照高層抽象進行解釋的異常。
以下所示:
try { } catch (LowerLevelException e) { throw new HigherLevelException(...); }
或者:
Iterator i = ... try { return i.next() } catch (NoSuchElementException e) { throw new IndexOutOfBoundsException("Index: " + index); }
還有一種特殊的異常轉義形式,稱爲異常鏈(exception chaining),若是低層的異常對於調試致使高層異常的問題很是有幫助,使用異常鏈就很合適。底層異常的緣由能夠傳給高層異常,高層異常提供訪問方法,來得到底層異常:
try { } catch (LowerLevelException cause) { throw new HigherLevelException(cause); }
儘管異常轉移與不佳選擇地從底層傳遞異常的作法相比有所改進,可是它也不能被濫用。
若是可能,處理來自跌穿那個異常的最好作法是,在調用底層方法以前,前確保他們會執行成功,從而避免它們拋出異常。
當對象拋出異常後,一般指望這個對象仍然保持在一種定義良好的可用狀態之中,即便失敗是發生在執行某個操做過程當中間,對於受檢異常而言,這點尤其重要,由於API用戶指望能從這種異常中恢復。
通常而言,失敗的方法調用應該使對象保持在被調用以前的狀態。具備這種屬性的方法被稱爲具備失敗原子性(failure atomic)。
有幾種方法能夠實現這種效果:
最簡單的辦法是設計一個不可變對象;
設計處理過程的順序,使得任何可能會失敗的部分都在對象狀態改變以前發生;
編寫一段恢復代碼(recovery code);
在對象的一份臨時拷貝上執行操做;
始終要單獨聲明受檢的異常,而且利用JavaDoc的@throws標記,準確地記錄下拋出每一個異常的條件。
永遠不要throws Exception,或者throws Throwable,它們會掩蓋該方法拋出的任何其餘異常。
未受檢的異常一般表明編程上的錯誤,讓API用戶瞭解全部這些錯誤都有助於幫助他們避免調用錯誤。對於方法可能拋出的未受檢異常,若是將這些異常信息很好的組織成列表文檔,就能夠有效地描述出這個方法被成功執行的前提條件(precondition)。
對於接口中的方法,在文檔中記錄下它可能拋出的未受檢異常顯得尤其重要。這份文檔構成了該接口的通用約定(general contract)的一部分,它指定來該接口的多個實現必須遵照的公共行爲。
使用JavaDoc的@throws標籤記錄下一個方法可能拋出的每一個未受檢異常,可是不要使用throws關鍵字將未受檢的異常包含在方法的聲明中。
若是一個類中的許多方法處於一樣的緣由拋出同一個異常,在類的文檔注視中,堆這個異常創建文檔。
空的catch達不到應有的目的,至少,catch塊也應該包含一條說明,解釋爲何忽略這個異常。
但願不要忽略異常。
java.lang.RuntimeException的直接子類有這些: