Effective Java (異常處理)
五十七、只針對(duì)異常情況才使用異常:
不知道你否則遇見過下面的代碼:
try {
int i = 0;3
while (true)
range[i++].climb();
}
catch (ArrayIndexOutOfBoundsException e) {
}
這段代碼的意圖不是很明顯,其本意就是遍歷變量數(shù)組range中的每一個(gè)元素,并執(zhí)行元素的climb方法,當(dāng)下標(biāo)超出range的數(shù)組長(zhǎng)度時(shí),將會(huì)直接拋出ArrayIndexOutOfBoundsException異常,catch代碼塊將會(huì)捕獲到該異常,但是未作任何處理,只是將該錯(cuò)誤視為正常工作流程的一部分來看待。這樣的寫法確實(shí)給人一種匪夷所思的感覺,讓我們?cè)賮砜匆幌滦薷暮蟮膶懛ǎ?BR>
for (Mountain m : range) {
m.climb();
}
和之前的寫法相比其可讀性不言而喻。那么為什么又有人會(huì)用第一種寫法呢?顯然他們是被誤導(dǎo)了,他們企圖避免for-each循環(huán)中JVM對(duì)每次數(shù)組訪問都要進(jìn)行的越界檢查。這無疑是多余的,甚至適得其反,因?yàn)閷⒋a放在try-catch塊中反而阻止了JVM的某些特定優(yōu)化,至于數(shù)組的邊界檢查,現(xiàn)在很多JVM實(shí)現(xiàn)都會(huì)將他們優(yōu)化掉了。在實(shí)際的測(cè)試中,我們會(huì)發(fā)現(xiàn)采用異常的方式其運(yùn)行效率要比正常的方式慢很多。
除了剛剛提到的效率和代碼可讀性問題,第一種寫法還會(huì)掩蓋一些潛在的Bug,假設(shè)數(shù)組元素的climb方法中也會(huì)訪問某一數(shù)組,并且在訪問的過程中出現(xiàn)了數(shù)組越界的問題,基于該錯(cuò)誤,JVM將會(huì)拋出ArrayIndexOutOfBoundsException異常,不幸的是,該異常將會(huì)被climb函數(shù)之外catch語句捕獲,在未做任何處理之后,就按照正常流程繼續(xù)執(zhí)行了,這樣Bug也就此被隱藏起來。
這個(gè)例子的教訓(xùn)很簡(jiǎn)單:"異常應(yīng)該只用于異常的情況下,它們永遠(yuǎn)不應(yīng)該用于正常的控制流"。雖然有的時(shí)候有人會(huì)說這種怪異的寫法可以帶來性能上的提升,即便如此,隨著平臺(tái)實(shí)現(xiàn)的不斷改進(jìn),這種異常模式的性能優(yōu)勢(shì)也不可能一直保持。然而,這種過度聰明的模式帶來的微妙的Bug,以及維護(hù)的痛苦卻依然存在。
根據(jù)這條原則,我們?cè)谠O(shè)計(jì)API的時(shí)候也是會(huì)有所啟發(fā)的。設(shè)計(jì)良好的API不應(yīng)該**它的客戶端為了正常的控制流而使用異常。如Iterator,JDK在設(shè)計(jì)時(shí)充分考慮到這一點(diǎn),客戶端在執(zhí)行next方法之前,需要先調(diào)用hasNext方法已確認(rèn)是否還有可讀的集合元素,見如下代碼:
for (Iterator i = collection.iterator(); i.hasNext(); ) {
Foo f = i.next();
}
如果Iterator缺少hasNext方法,客戶端則將**改為下面的寫法:
try {
Iterator i = collection.iterator();
while (true)
Foo f = i.next();
}
catch (NoSuchElementException e) {
}
這應(yīng)該非常類似于本條目開始時(shí)給出的遍歷數(shù)組的例子。在實(shí)際的設(shè)計(jì)中,還有另外一種方式,即驗(yàn)證可識(shí)別的錯(cuò)誤返回值,然而該方式并不適合于此例,因?yàn)閷?duì)于next,返回null可能是合法的。那么這兩種設(shè)計(jì)方式在實(shí)際應(yīng)用中有哪些區(qū)別呢?
1. 如果是缺少同步的并發(fā)訪問,或者可被外界改變狀態(tài),使用可識(shí)別返回值的方法是非常必要的,因?yàn)樵跍y(cè)試狀態(tài)(hasNext)和對(duì)應(yīng)的調(diào)用(next)之間存在一個(gè)時(shí)間窗口,在該窗口中,對(duì)象可能會(huì)發(fā)生狀態(tài)的變化。因此,在該種情況下應(yīng)選擇返回可識(shí)別的錯(cuò)誤返回值的方式。
2. 如果狀態(tài)測(cè)試方法(hasNext)和相應(yīng)的調(diào)用方法(next)使用的是相同的代碼,出于性能上的考慮,沒有必要重復(fù)兩次相同的工作,此時(shí)應(yīng)該選擇返回可識(shí)別的錯(cuò)誤返回值的方式。
3. 對(duì)于其他情形則應(yīng)該盡可能考慮"狀態(tài)測(cè)試"的設(shè)計(jì)方式,因?yàn)樗梢詭砀玫目勺x性。
五十八、對(duì)可恢復(fù)的情況使用受檢異常,對(duì)編程錯(cuò)誤使用運(yùn)行時(shí)異常:
Java中提供了三種可拋出結(jié)構(gòu):受檢異常、運(yùn)行時(shí)異常和錯(cuò)誤。該條目針對(duì)這三種類型適用的場(chǎng)景給出了一般性原則。
1. 如果期望調(diào)用者能夠適當(dāng)?shù)鼗謴?fù),對(duì)于這種情況就應(yīng)該使用受檢異常,如某人打算網(wǎng)上購(gòu)物,結(jié)果余額不足,此時(shí)可以拋出自定義的受檢異常。通過拋出受檢異常,將**調(diào)用者在catch子句中處理該異常,或繼續(xù)向上傳播。因此,在方法中聲明受檢異常,是對(duì)API用戶的一種潛在提示。
2. 用運(yùn)行時(shí)異常來表明編程錯(cuò)誤。大多數(shù)的運(yùn)行時(shí)異常都表示"前提違例",即API的使用者沒有遵守API設(shè)計(jì)者建立的使用約定。如數(shù)組訪問越界等問題。
3. 對(duì)于錯(cuò)誤而言,通常是被JVM保留用于表示資源不足、約束失敗,或者其他使程序無法繼續(xù)執(zhí)行的條件。
針對(duì)自定義的受檢異常,該條目還給出一個(gè)非常實(shí)用的技巧,當(dāng)調(diào)用者捕獲到該異常時(shí),可以通過調(diào)用該自定義異常提供的接口方法,獲取更為具體的錯(cuò)誤信息,如當(dāng)前余額等信息。
五十九、避免不必要的使用受檢異常:
受檢異常是Java提供的一個(gè)很好的特征。與返回值不同,它們**程序員必須處理異常的條件,從而大大增強(qiáng)了程序的可靠性。然而,如果過分使用受檢異常則會(huì)使API在使用時(shí)非常不方便,畢竟我們還是需要用一些額外的代碼來處理這些拋出的異常,倘若在一個(gè)函數(shù)中,它所調(diào)用的五個(gè)API都會(huì)拋出異常,那么編寫這樣的函數(shù)代碼將會(huì)是一項(xiàng)令人沮喪的工作。
如果正確的使用API不能阻止這種異常條件的產(chǎn)生,并且一旦產(chǎn)生異常,使用API的程序員可以立即采用有用的動(dòng)作,這種負(fù)擔(dān)就被認(rèn)為是正當(dāng)?shù)摹3沁@兩個(gè)條件都成立,否則更適合使用未受檢異常,見如下測(cè)試:
try {
dosomething();
} catch (TheCheckedException e) {
throw new AssertionError();
}
try {
donsomething();
} catch (TheCheckedException e) {
e.printStackTrace();
System.exit(1);
}
當(dāng)我們使用受檢異常時(shí),如果在catch子句中對(duì)異常的處理方式僅僅如以上兩個(gè)示例,或者還不如它們的話,那么建議你考慮使用未受檢異常。原因很簡(jiǎn)單,它們?cè)赾atch子句中,沒有做出任何用于恢復(fù)異常的動(dòng)作。
六十、優(yōu)先使用標(biāo)準(zhǔn)異常:
使用標(biāo)準(zhǔn)異常,不僅可以更好的復(fù)用已有的代碼,同時(shí)也使你設(shè)計(jì)的API更加容易學(xué)習(xí)和使用,因?yàn)樗统绦騿T已經(jīng)熟悉的習(xí)慣用法更為一致。另外一個(gè)優(yōu)勢(shì)是,代碼的可讀性更好,程序員在閱讀時(shí)不會(huì)出現(xiàn)更多的不熟悉的代碼。該條目給出了一些非常常用且容易被復(fù)用的異常,見下表:
異常 應(yīng)用場(chǎng)合
IllegalArgumentException 非null的參數(shù)值不正確。
IllegalStateException 對(duì)于方法調(diào)用而言,對(duì)象狀態(tài)不合適。
NullPointerException 在禁止使用null的情況下參數(shù)值為null。
IndexOutOfBoundsException 下標(biāo)參數(shù)值越界
ConcurrentModificationException 在禁止并發(fā)修改的情況下,檢測(cè)到對(duì)象的并發(fā)修改。
UnsupportedOperationException 對(duì)象不支持用戶請(qǐng)求的方法。
當(dāng)然在Java中還存在很多其他的異常,如ArithmeticException、NumberFormatException等,這些異常均有各自的應(yīng)用場(chǎng)合,然而需要說明的是,這些異常的應(yīng)用場(chǎng)合在有的時(shí)候界限不是非常分明,至于該選擇哪個(gè)比較合適,則更多的需要依賴上下文環(huán)境去判斷。
最后需要強(qiáng)調(diào)的是,一定要確保拋出異常的條件和該異常文檔中描述的條件保持一致。
六十一、拋出與抽象相對(duì)應(yīng)的異常:
如果方法拋出的異常與它所執(zhí)行的任務(wù)沒有明顯的關(guān)系,這種情形將會(huì)使人不知所措。特別是當(dāng)異常從底層開始拋出時(shí),如果在中間層沒有做任何處理,這樣底層的實(shí)現(xiàn)細(xì)節(jié)將會(huì)直接污染高層的API接口。為了解決這樣的問題,我們通常會(huì)做出如下處理:
try {
doLowerLeverThings();
} catch (LowerLevelException e) {
throw new HigherLevelException(...);
}
這種處理方式被稱為異常轉(zhuǎn)譯。事實(shí)上,在Java中還提供了一種更為方便的轉(zhuǎn)譯形式--異常鏈。試想一下上面的示例代碼,在調(diào)試階段,如果高層應(yīng)用邏輯可以獲悉到底層實(shí)際產(chǎn)生異常的原因,那么對(duì)找到問題的根源將會(huì)是非常有幫助的,見如下代碼:
try { doLowerLevelThings();
} catch (LowerLevelException cause) {
throw new HigherLevelException(cause);
}
底層異常作為參數(shù)傳遞給了高層異常,對(duì)于大多數(shù)標(biāo)準(zhǔn)異常都支持異常鏈的構(gòu)造器,如果沒有,可以利用Throwable的initCause方法設(shè)置原因。異常鏈不僅讓你可以通過接口函數(shù)getCause訪問原因,它還可以將原因的堆棧軌跡集成到更高層的異常中。
通過這種異常鏈的方式,可以非常有效的將底層實(shí)現(xiàn)細(xì)節(jié)與高層應(yīng)用邏輯徹底分離出來。
六十三、在細(xì)節(jié)中包含能捕獲失敗的信息:
當(dāng)程序由于未被捕獲的異常而失敗的時(shí)候,系統(tǒng)會(huì)自動(dòng)地打印出該異常的堆棧軌跡。在堆棧軌跡中包含該異常的字符串表示法,即toString方法的返回結(jié)果。如果我們?cè)诖藭r(shí)為該異常提供了詳細(xì)的出錯(cuò)信息,那么對(duì)于錯(cuò)誤定位和追根溯源都是極其有意義的。比如,我們將拋出異常的函數(shù)的輸入?yún)?shù)和函數(shù)所在類的域字段值等信息格式化后,再打包傳遞給待拋出的異常對(duì)象。假設(shè)我們的高層應(yīng)用捕捉到IndexOutOfBoundsException異常,如果此時(shí)該異常對(duì)象能夠攜帶數(shù)組的下界和上界,以及當(dāng)前越界的下標(biāo)值等信息,在看到這些信息后,我們就能很快做出正確的判斷并修訂該Bug。
特別是對(duì)于受檢異常,如果拋出的異常類型還能提供一些額外的接口方法用于獲取導(dǎo)致錯(cuò)誤的數(shù)據(jù)或信息,這對(duì)于捕獲異常的調(diào)用函數(shù)進(jìn)行錯(cuò)誤恢復(fù)是非常重要的。
六十四、努力使失敗保持原子性:
這是一個(gè)非常重要的建議,因?yàn)樵趯?shí)際開發(fā)中當(dāng)你是接口的開發(fā)者時(shí),經(jīng)常會(huì)忽視他,認(rèn)為不保證的話估計(jì)也沒有問題。相反,如果你是接口的使用者,也同樣會(huì)忽略他,會(huì)認(rèn)為這個(gè)是接口實(shí)現(xiàn)者理所應(yīng)當(dāng)完成的事情。
當(dāng)對(duì)象拋出異常之后,通常我們期望這個(gè)對(duì)象仍然保持在一種定義良好的可用狀態(tài)之中,即使失敗是發(fā)生在執(zhí)行某個(gè)操作的過程中間。對(duì)于受檢異常而言,這尤為重要,因?yàn)檎{(diào)用者希望能從這種異常中進(jìn)行恢復(fù)。一般而言,失敗的方法調(diào)用應(yīng)該使對(duì)象保持在被調(diào)用之前的狀態(tài)。具有這種屬性的方法被稱為具有"失敗原子性"。
有以下幾種途徑可以保持這種原子性。
1. 最簡(jiǎn)單的方法是設(shè)計(jì)不可變對(duì)象。因?yàn)槭〉牟僮髦粫?huì)導(dǎo)致新對(duì)象的創(chuàng)建失敗,而不會(huì)影響已有的對(duì)象。
2. 對(duì)于可變對(duì)象,一般方法是在操作該對(duì)象之前先進(jìn)行參數(shù)的有效性驗(yàn)證,這可以使對(duì)象在被修改之前,拋出更為有意義的異常,如:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}
如果沒有在操作之前驗(yàn)證size,elements的數(shù)組也會(huì)拋出異常,但是由于size的值已經(jīng)發(fā)生了變化,之后再繼續(xù)使用該對(duì)象時(shí)將永遠(yuǎn)無法恢復(fù)到正常狀態(tài)了。
3. 預(yù)先寫好恢復(fù)性代碼,在出現(xiàn)錯(cuò)誤時(shí)執(zhí)行帶段代碼,由于此方法在代碼編寫和代碼維護(hù)的過程中,均會(huì)帶來很大的維護(hù)開銷,再加之效率相對(duì)較低,因此很少會(huì)使用該方法。
4. 為該對(duì)象創(chuàng)建一個(gè)臨時(shí)的copy,一旦操作過程中出現(xiàn)異常,就用該復(fù)制對(duì)象重新初始化當(dāng)前的對(duì)象的狀態(tài)。
雖然在一般情況下都希望實(shí)現(xiàn)失敗原子性,然而在有些情況下卻是難以做到的,如兩個(gè)線程同時(shí)修改一個(gè)可變對(duì)象,在沒有很好同步的情況下,一旦拋出ConcurrentModificationException異常之后,就很難在恢復(fù)到原有狀態(tài)了。
六十五、不要忽略異常:
這是一個(gè)顯而易見的常識(shí),但是經(jīng)常會(huì)被違反,因此該條目重新提出了它,如:
try {
dosomething();
} catch (SomeException e) {
}
可預(yù)見的、可以使用忽略異常的情形是在關(guān)閉FileInputStream的時(shí)候,因?yàn)榇藭r(shí)數(shù)據(jù)已經(jīng)讀取完畢。即便如此,如果在捕獲到該異常時(shí)輸出一條提示信息,這對(duì)于挖出一些潛在的問題也是非常有幫助的。否則一些潛在的問題將會(huì)一直隱藏下去,直到某一時(shí)刻突然爆發(fā),以致造成難以彌補(bǔ)的后果。
該條目中的建議同樣適用于受檢異常和未受檢的異常。
相關(guān)文章
SpringMVC使用注解實(shí)現(xiàn)登錄功能
這篇文章主要為大家詳細(xì)介紹了SpringMVC使用注解實(shí)現(xiàn)登錄功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-09-09Triple協(xié)議支持Java異?;貍髟O(shè)計(jì)實(shí)現(xiàn)詳解
這篇文章主要為大家介紹了Triple協(xié)議支持Java異?;貍髟O(shè)計(jì)實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12Ubuntu安裝jenkins完成自動(dòng)化構(gòu)建詳細(xì)步驟
Jenkins是一個(gè)開源的自動(dòng)化服務(wù)器,可以用來輕松地建立持續(xù)集成和持續(xù)交付(CI/CD)管道,這篇文章主要給大家介紹了關(guān)于Ubuntu安裝jenkins完成自動(dòng)化構(gòu)建的相關(guān)資料,需要的朋友可以參考下2024-03-03SpringBoot整合Retry實(shí)現(xiàn)錯(cuò)誤重試過程逐步介紹
重試的使用場(chǎng)景比較多,比如調(diào)用遠(yuǎn)程服務(wù)時(shí),由于網(wǎng)絡(luò)或者服務(wù)端響應(yīng)慢導(dǎo)致調(diào)用超時(shí),此時(shí)可以多重試幾次。用定時(shí)任務(wù)也可以實(shí)現(xiàn)重試的效果,但比較麻煩,用Spring Retry的話一個(gè)注解搞定所有,感興趣的可以了解一下2023-02-02詳談hibernate,jpa與spring?data?jpa三者之間的關(guān)系
這篇文章主要介紹了hibernate,jpa與spring?data?jpa三者之間的關(guān)系,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11從Hello?World開始理解GraphQL背后處理及執(zhí)行過程
這篇文章主要為大家介紹了從Hello?World開始理解GraphQL背后處理過程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-0829個(gè)要點(diǎn)幫你完成java代碼優(yōu)化
本文給大家分享的是個(gè)人總結(jié)的29個(gè)java優(yōu)化需要注意的地方,非常的全面細(xì)致,推薦給大家,有需要的小伙伴可以參考下2015-03-03