第五十七條、只針對異常的情況才使用異常
-
不要優(yōu)先使用基于異常的模式:
- 因為異常機制的設計初衷是用于不正常的情況,所以很少會有JVM實現(xiàn)對它們進行優(yōu)化,使得與顯式的測試一樣快速;
- 把代碼放在try-catch塊中反而組織了現(xiàn)代JVM實現(xiàn)本來可能要執(zhí)行的某些特定優(yōu)化;
- 對數(shù)組進行遍歷的標準模式并不會導致冗余的檢查,有些現(xiàn)代的JVM實現(xiàn)會將他們優(yōu)化掉。
異常應該只用于異常的情況下,它們永遠不應該用于正常的控制流。
-
設計良好的API不應該強迫它的客戶端為了正常的控制流而使用異常。如果類具有狀態(tài)相關的方法,即只有在特定的不可預知的條件下才可以調用的方法。
這個類也應該有單獨的狀態(tài)測試方法,即指示是否可以調用這個狀態(tài)相關的方法。
或者如果狀態(tài)相關的方法被調用時,該對象處于不適當?shù)臓顟B(tài)中,它就會返回一個可識別的值,如null。
-
對于“狀態(tài)測試方法”和“可識別的返回值”兩種做法如何選擇:
- 如果對象將在缺少外部同步的情況下被并發(fā)訪問,或者可被外界改變狀態(tài),使用可被識別的返回值可能是很有必要的。因為在調用“狀態(tài)測試”方法和調用對應的“狀態(tài)相關”方法的時間間隔中,對象的狀態(tài)可能發(fā)生變化。
- 如果單獨的“狀態(tài)測試”方法必須重復“狀態(tài)相關”方法的工作,從性能的角度考慮,就應該使用可被識別的返回值。如果所有其他方面都是等同的,那么“狀態(tài)測試方法”則略優(yōu)于可被識別的返回值。因為它提供了更好的可讀性,對于使用不當?shù)那樾慰赡芨尤菀讬z測和改正。
第五十八條、對可恢復的情況使用受檢異常,對編程錯誤使用運行時異常
Java程序設計語言提供了三種可拋出結構:受檢的異常(checked exception)、運行時異常(run-time exception)和錯誤(error)。關于在什么時候拋出何種結構,有些一般性的原則可以指導。
如果期望調用者能夠適當?shù)鼗謴停瑢τ谶@種情況就應該使用受檢的異常。通過拋出受檢的異常,強迫調用者在一個catch字句中處理該異常,或者將它傳播出去。因此,方法中聲明要拋出的每個受檢的異常,都是對API用戶的一種潛在指示:與異常相關聯(lián)的條件是調用這個方法的一種可能結果。
-
另外兩種未受檢的可拋出結構:運行時異常和錯誤。在行為上兩者是等同的:它們都是不需要也不應該被捕獲的可拋出結構。這種情況下屬于不可恢復的情形,繼續(xù)執(zhí)行下去有害無益。如果程序沒有捕捉到這樣的可拋出結構,將會導致當前的線程停止halt,并出現(xiàn)適當?shù)腻e誤信息。
- 用運行時異常來表明編程錯誤。大多數(shù)的運行時異常都表示前提違例(precondition violation)即API的客戶沒有遵守API規(guī)范建立的約定。
- 錯誤類型往往被JVM保留用于表示資源不足、約束失敗,或者其他使程序無法繼續(xù)執(zhí)行的條件。由于這已經(jīng)是個幾乎被普遍接受的慣例,所以最好不要再實現(xiàn)任何新的Error子類,自己實現(xiàn)的所有未受檢的拋出結構都應該是RuntimeException的子類。
對于可恢復的情況使用受檢異常,對編程錯誤使用運行時異常。異常完全意義上也是個對象,可以在它上面定義任何的方法。這些方法的主要用途是為捕獲異常的代碼而提供額外的信息,特別是引發(fā)這個異常條件的信息。
第五十九條、避免不必要地使用受檢的異常
受檢的異常是Java程序語言的一項很好的特性,它們強迫程序員處理異常的條件,大大增強了可靠性。但是,過分地使用受檢的異常會使API使用起來非常不方便。如果一個方法拋出一個或者多個受檢的異常,調用該方法的代碼必須在一個或多個catch塊中處理這些異常,或者它必須聲明它拋出這些異常,并將它們傳播出去,給程序員增添了負擔。
何時使用受檢的異常:如果正確地使用API并不能阻止這種異常條件的產(chǎn)生,一旦產(chǎn)生異常,使用API的程序員可以立即采取有用的動作。兩種條件缺一不可。
第六十條、優(yōu)先使用標準的異常
Java平臺類庫提供了一組基本的未受檢異常,它們滿足了絕大多數(shù)API的異常拋出需要。
-
重用現(xiàn)有異常的好處:
- 它使你的API更容易學習和使用;
- 對于用到這些API的程序而言,它們的可讀性更好;
- 異常類越少,意味著內存印跡越小,裝載這些類的時間開銷越小。
-
常用的異常:
-
IllegalArgumentException
:當調用者傳遞的參數(shù)不合適的時候,往往會拋出這個異常; -
IllegalStateException
:如果因為接收對象的狀態(tài)而使調用非法,通常會拋出這個異常; -
NullPointerException
:禁止使用null的情況下參數(shù)值為null; -
IndexOutOfBoundsException
:下標參數(shù)越界; -
ConcurrentModificationException
:在禁止并發(fā)修改的情況下,檢測到對象的并發(fā)修改; -
UnsupportedOperationException
:對象不支持用戶請求的方法。
-
第六十一條、拋出與抽象相對應的異常
-
更高層的實現(xiàn)應該捕獲底層的異常,同時拋出可以按照高層抽象進行解釋的異常。這種做法成為異常轉譯exception translation。
try{ ... }catch(LowerLevelException e){ throw new HigherLevelException(...); }
一種特殊的異常轉譯的形式稱為異常鏈,低層的異常原因被傳到高層的異常,高層的異常提供訪問方法來獲得低層的異常。大多數(shù)標準的異常都有支持鏈的構造器,對于沒有支持異常鏈的異常,可以利用Throwable的initCause方法設置原因。異常鏈不僅讓你可以通過程序訪問原因,它還可以將原因的堆棧軌跡集成到更高層的異常中。
異常轉譯不能濫用,如有可能,處理來自低層異常的最好做法是,在調用低層方法之前確保它們會成功執(zhí)行,從而避免它們拋出異常。如果無法避免低層異常,次選方案是讓高層悄悄繞過這些異常,從而將高層方法的調用者與低層的問題隔離開,在這種情況下,可以用適當?shù)挠涗洐C制(如:java.util.logging)將異常記錄下來。
總結:如果不能阻止或者處理來自更低層的異常,一般的做法是異常轉譯,除非低層方法碰巧可以保證它拋出的所有異常對高層也合適才可以將異常從底層傳播到高層。異常鏈對高層和低層異常都提供了最佳的功能:它允許拋出適當?shù)母邔赢惓#瑫r又能捕獲底層的原因進行失敗分析。
第六十二條、每個方法拋出的異常都要有文檔
始終要單獨地聲明受檢的異常,并且利用Javadoc的
@throws
標記,準確地記錄下拋出每個異常的條件。永遠不要聲明一個方法throws Exception:這樣的方法聲明不進沒有為程序員提供“關于這個方法能夠拋出哪些異常”的任何指導信息,并且大大妨礙了該方法的使用,因為它實際上掩蓋了該方法在同樣的執(zhí)行環(huán)境下可能拋出的任何其他異常。未受檢的異常通常代表編程上的錯誤,讓程序員了解所有這些錯誤都有利于幫助他們避免繼續(xù)犯錯。使用
@throws
標簽記錄下一個方法可能拋出的每個非受檢異常,但是不要使用throws關鍵字將未受檢的異常包含在方法的聲明中。使用API的程序員需要知道哪些是受檢的哪些是未受檢的。但是為每個方法可能拋出的所有未受檢的異常建立文檔是很理想的,實踐中并未總能做到這一點。如果一個類中的許多方法出于同樣的原因而拋出同一個異常,在該類的文檔注釋中對這個異常建立文檔是可以接受的。
第六十三條、在細節(jié)消息中包含能捕獲失敗的信息
當程序由于未被捕獲的異常而失敗的時候,系統(tǒng)會自動地打印出該異常的堆棧軌跡,在堆棧軌跡中包含該異常的字符串表示法,即它的toString方法的調用結果。它通常包含該異常的類名,緊隨其后的是細節(jié)消息(detail message)。異常的細節(jié)消息應該能夠捕獲住失敗,便于以后分析。
異常的細節(jié)消息應該包括所有“對該異常有貢獻”的參數(shù)和域的值。但是不應該太過冗余。異常的細節(jié)消息不應該與“用戶層次的錯誤信息”混為一談,后者對于最終用戶而言必須是可理解的。異常的字符串表示法主要是讓程序員或者是域服務人員來分析失敗的原因。因此,信息的內容比可理解性要重要得多。
方法:在異常的構造器而不是字符串細節(jié)消息中引入這些信息,然后有了這些信息,只要把它們放入消息描述中,就可以自動產(chǎn)生細節(jié)信息。
第六十四條、努力使失敗保持原子性
一般,失敗的方法調用應該使對象保持在被調用之前的狀態(tài)。具有這種屬性的方法稱為具有失敗原子性。
-
實現(xiàn)的幾種途徑:
- 最簡單的方法:設計一個不可變的對象。(失敗原子性是必然的)
- 對于可變對象執(zhí)行操作的方法,在執(zhí)行操作之前檢查參數(shù)的有效性,這可以使在對象的狀態(tài)被修改之前先拋出適當?shù)漠惓!?/li>
- 一般的方法:調整計算處理過程的順序,使得任何有可能失敗的計算部分都在對象狀態(tài)被修改之前發(fā)生。
- 編寫一段恢復代碼,由它來攔截操作過程中發(fā)生的失敗,以及使對象回滾到操作開始之前的狀態(tài)上。(主要用于永久性的(基于磁盤的)數(shù)據(jù)結構)
- 在對象的一份臨時拷貝上執(zhí)行操作,當操作完成后再用臨時拷貝的結果代替對象的內容。例如:
Collections.sort
在執(zhí)行排序之前,首先把它的輸入列表轉入到一個數(shù)組中,以便降低在排序的內循環(huán)中訪問元素所需要的開銷。
一般情況下都希望實現(xiàn)失敗原子性,但是并非總是可以做到的。例如兩個線程企圖在沒有適當?shù)耐綑C制的情況下,并發(fā)地修改同一個對象。錯誤(相對于異常)通常是不可恢復的,當方法拋出錯誤時,它們不需要努力保持失敗原子性。
第六十五條、不要忽略異常
當API的設計者聲明一個方法將要拋出某個異常時,他們等于在試圖說明某些事情,所以不要用一個空的catch塊來忽略它。會使異常得不到應有的目的。
有一條情況可以忽略:即關閉FileInputStream的時候,因為還沒有改變文件的狀態(tài),因此不必執(zhí)行任何恢復動作,并且文件中讀取到所需要的信息,因此不必終止正在進行的操作,但是此時把異常記錄下來是一個明智的做法,可以因此調查異常的原因。