前言
異常處理的問(wèn)題之一是知道何時(shí)以及如何去使用它趾撵。我會(huì)討論一些異常處理的最佳實(shí)踐刊苍,也會(huì)總結(jié)最近在異常處理上的一些爭(zhēng)論。
作為程序員仲吏,我們想要寫高質(zhì)量的能夠解決問(wèn)題的代碼不铆。但是,異常經(jīng)常是伴隨著代碼產(chǎn)生的副作用裹唆。沒(méi)有人喜歡副作用誓斥,因此我們會(huì)試圖用自己的方式來(lái)解決這個(gè)問(wèn)題。我看過(guò)不少的程序用下面的方法應(yīng)對(duì)異常:
上面這段代碼的問(wèn)題在哪里许帐?
一旦一個(gè)異常被拋出之后劳坑,正常的執(zhí)行流程會(huì)停止并且將控制交給捕捉塊。捕捉塊捕獲異常成畦,然后只是把它的信息打印了一下距芬。之后程序正常運(yùn)行,就像沒(méi)有任何事情發(fā)生一樣循帐。
那下面的這種方法呢框仔?
這是一個(gè)空方法,里面沒(méi)有任何的代碼拄养。為什么一個(gè)空方法能夠拋出異常存和?JAVA并不阻止你這么做。最近衷旅,我遇到了一些和這個(gè)很相似的代碼捐腿,明明代碼塊中沒(méi)有拋出異常的語(yǔ)句,卻在方法聲明中拋出異常柿顶。當(dāng)我問(wèn)開(kāi)發(fā)人員為什么這么做茄袖,他會(huì)回答“我知道這樣會(huì)影響API,但是我之前就這么做的而且效果還不錯(cuò)”嘁锯。
C 社區(qū)花了好久才決定如何使用異常宪祥。這場(chǎng)爭(zhēng)論也在JAVA社區(qū)產(chǎn)生了聂薪。我看到不少JAVA開(kāi)發(fā)人員艱難的使用異常。如果不能夠正確使用的話蝗羊,異常會(huì)影響程序的性能藏澳,因?yàn)樗枰褂脙?nèi)存和CPU來(lái)創(chuàng)建,拋出以及捕獲耀找。如果過(guò)度使用的話翔悠,會(huì)使得代碼難以閱讀,并且影響API的使用人員野芒。我們都知道這將會(huì)帶來(lái)代碼漏洞以及壞味道蓄愁〗ρ客戶端代碼常會(huì)通過(guò)忽略這個(gè)異臣司郑或是直接將其拋出來(lái)避開(kāi)這個(gè)問(wèn)題均牢,就像之前的兩個(gè)例子那樣侧漓。
小編是一個(gè)有著5年工作經(jīng)驗(yàn)的java程序員钞翔,對(duì)于java驾霜,自己有做資料的整合赶盔,一個(gè)完整學(xué)習(xí)java的路線斤程,學(xué)習(xí)資料和工具荸恕,相信這里有很多學(xué)習(xí)java的小伙伴咽笼,我創(chuàng)立了一個(gè)2000人學(xué)習(xí)扣群,479121291戚炫。每晚都有java的直播課程剑刑。無(wú)論是初級(jí)還是進(jìn)階的小伙伴小編我都?xì)g迎!
異常的本質(zhì)
從廣義的角度來(lái)說(shuō)双肤,一共有三種不同的場(chǎng)景會(huì)導(dǎo)致異常的產(chǎn)生:
編程錯(cuò)誤導(dǎo)致的異常:這一類的異常是因?yàn)椴磺‘?dāng)?shù)木幊處?lái)的(比如 , )施掏。客戶端通常無(wú)法對(duì)這些錯(cuò)誤采取任何措施
客戶端代碼的錯(cuò)誤:客戶端代碼在API允許的范圍之外使用API茅糜,從而違背了合約七芭。客戶端可以通過(guò)異常中提供的有用信息蔑赘,采用一些替代方法狸驳。比如,當(dāng)解析格式不正確的XML文件時(shí)缩赛,會(huì)拋出異常耙箍。這個(gè)異常中包含導(dǎo)致該錯(cuò)誤發(fā)生的XML內(nèi)容的具體位置∷肘桑客戶端可以通過(guò)這些信息采取回復(fù)措施辩昆。
資源失效導(dǎo)致的異常:比如系統(tǒng)內(nèi)存不足或是網(wǎng)絡(luò)連接失敗≈继唬客戶端面對(duì)資源失效的回應(yīng)是要根據(jù)上下文來(lái)決定的汁针∈醴客戶端可以在一段時(shí)間之后試著重新連接或是記錄資源失效日志然后暫停應(yīng)用程序。
JAVA異常類型
JAVA定義了兩種異常:
需檢查的異常:從 類繼承的異常都是需檢查異常施无』源剩客戶端需要處理API拋出的這一類異常,通過(guò)try-catch或是繼續(xù)拋出猾骡。
無(wú)需檢查的異常: 也是 的子類瑞躺。但是,繼承了 的類受到了特殊的待遇卓练“客戶端代碼無(wú)需專門處理這一類異常购啄。
下圖展示了 的繼承樹(shù):
上圖中襟企, 繼承自 ,因此它也是一個(gè)無(wú)需檢查的異常狮含。
我看到過(guò)大量使用需檢查異常只在極少數(shù)時(shí)候使用無(wú)需檢查異常的顽悼。最近,JAVA社區(qū)在需檢查異常的真正價(jià)值上爆發(fā)了熱烈的討論几迄。這場(chǎng)辯論源于JAVA是第一個(gè)包含需檢查異常的主流OO框架蔚龙。C 和C#根本沒(méi)有需檢查異常。這些語(yǔ)言中所有的異常都是無(wú)需檢查的映胁。
從低層拋出的需檢查異常強(qiáng)制要求調(diào)用方捕獲或是拋出該異常木羹。如果客戶端不能有效的處理該異常,API和客戶端之間的異常協(xié)議將會(huì)帶來(lái)極大的負(fù)擔(dān)解孙】犹睿客戶端的開(kāi)發(fā)人員可能會(huì)通過(guò)將異常抑制在一個(gè)空的捕獲塊中或是直接拋出它。從而又將這個(gè)負(fù)擔(dān)交給了客戶端的調(diào)用方弛姜。
還有人指責(zé)需檢查異常會(huì)破壞封裝脐瑰,看下面這段代碼:
方法拋出了兩個(gè)需檢查異常。調(diào)用這個(gè)方法的客戶端必須明確的處理這兩種具體的異常廷臼,即使它們并不清楚 內(nèi)究竟是哪個(gè)文件訪問(wèn)或是數(shù)據(jù)庫(kù)訪問(wèn)失敗了苍在,而且它們也沒(méi)有提供文件系統(tǒng)或是數(shù)據(jù)庫(kù)的邏輯。因此荠商,這樣的異常處理導(dǎo)致方法和調(diào)用者之前出現(xiàn)了不當(dāng)?shù)膹?qiáng)耦合寂恬。
設(shè)計(jì)API的最佳實(shí)踐
在討論了這些之后,我們可以來(lái)探討一下如何設(shè)計(jì)一個(gè)正確拋出異常的良好的API莱没。
1.在選擇拋出需確定異陈咏#或是無(wú)需確定異常時(shí),問(wèn)自己這樣的一個(gè)問(wèn)題:客戶端代碼在遇到異常時(shí)會(huì)進(jìn)行怎樣的處理郊愧?
如果客戶端能夠采取措施從這個(gè)異常中恢復(fù)過(guò)來(lái)朴译,那就選擇需確定異常井佑。如果客戶端不能采取有效的措施,就選擇無(wú)需確定異常眠寿。有效的措施是指從異常中恢復(fù)的措施躬翁,而不僅僅是記錄錯(cuò)誤日志。
除此以外盯拱,盡量選擇無(wú)需確定的異常:它的優(yōu)點(diǎn)在于不會(huì)強(qiáng)迫客戶端顯式地處理這種異常盒发。它會(huì)冒泡到任何你想捕獲它的地方。JAVA API提供了許多無(wú)需檢查的異常如 , 和 狡逢。我傾向于使用JAVA提供的標(biāo)準(zhǔn)的異常宁舰,盡量不去創(chuàng)建自己的異常。
2.保留封裝
永遠(yuǎn)不要將特定于實(shí)現(xiàn)的異常傳遞到更高層奢浑。比如蛮艰,不要將數(shù)據(jù)層的 傳遞出去。業(yè)務(wù)層不需要了解 雀彼。你有兩個(gè)選擇:
將 轉(zhuǎn)換為另一個(gè)需檢查異常壤蚜,如果客戶代碼需要從異常中恢復(fù)。
將 轉(zhuǎn)換為無(wú)需檢查異常徊哑,如果客戶端代碼無(wú)法對(duì)其進(jìn)行處理袜刷。
大多數(shù)時(shí)候,客戶代碼無(wú)法解決 莺丑。這時(shí)候就將其轉(zhuǎn)化為無(wú)需檢查的異常著蟹。
這里的catch塊并沒(méi)有做任何事情。不如通過(guò)如下的方式解決它:
這里將 轉(zhuǎn)化為了 梢莽。如果 出現(xiàn)了萧豆,catch塊就會(huì)拋出一個(gè)運(yùn)行時(shí)異常。當(dāng)前執(zhí)行的線程將會(huì)停止并報(bào)告該異常蟹漓。但是炕横,該異常并沒(méi)有影響到我的業(yè)務(wù)邏輯模塊,它無(wú)需進(jìn)行異常處理葡粒,更何況它根本無(wú)法對(duì) 進(jìn)行任何操作份殿。如果我的catch塊需要根異常原因,可以使用 方法嗽交。
如果你確信業(yè)務(wù)層可以采取補(bǔ)救措施卿嘲,你可以將其轉(zhuǎn)化為一個(gè)更有意義的無(wú)需檢查異常。但是我覺(jué)得拋出RuntimeException足以適用大多數(shù)的場(chǎng)景夫壁。
3.當(dāng)無(wú)法提供更加有用信息時(shí)拾枣,不要自定義異常
下面這段代碼有什么問(wèn)題?
它沒(méi)有給客戶端代碼提供任何有用的信息,除了一個(gè)稍微具有含義的命名梅肤。不要忘了 類和別的類一樣司蔬,在里面你可以添加一下方法供客戶端調(diào)用,獲得有用的信息姨蝴。
新版本的異常提供了兩個(gè)有用的方法: 俊啼,它會(huì)返回請(qǐng)求的名字,和 左医,它會(huì)返回一組相近的可用的用戶名授帕。客戶端可以使用這些方法來(lái)獲取有用的信息浮梢。但是如果你不準(zhǔn)備添加這些額外的信息跛十,那就拋出一個(gè)標(biāo)準(zhǔn)的異常即可。
如果你覺(jué)得客戶端代碼在記錄日志之外對(duì)這個(gè)異常不能進(jìn)行任何操作秕硝,那么最好拋出無(wú)需檢查異常:
除此以外芥映,你還可以提供一個(gè)方法來(lái)檢查用戶名是否已經(jīng)被使用。
4.文檔化異常
你可以使用Javadoc的 標(biāo)記來(lái)記錄需檢查異常和無(wú)需檢查異常缝裤。但是屏轰,我傾向于寫單元測(cè)試來(lái)文檔化異常颊郎。單元測(cè)試允許我在使用中查看異常憋飞,并且作為一個(gè)可以被執(zhí)行的文檔來(lái)使用。無(wú)論你采用哪種方法姆吭,盡量使你的客戶端代碼了解你的API會(huì)拋出的異常榛做。這里提供了 的單元測(cè)試。
上面這段代碼在調(diào)用 應(yīng)當(dāng)拋出 内狸。如果沒(méi)有拋出該異常检眯,則會(huì)執(zhí)行 顯式的說(shuō)明該測(cè)試失敗了。通過(guò)為異常編寫測(cè)試昆淡,你不僅能記錄異常如何觸發(fā)锰瘸,而且使你的代碼在經(jīng)過(guò)這些測(cè)試后更加健壯。
使用異常的最佳實(shí)踐
1.自覺(jué)清理資源
如果你在使用如數(shù)據(jù)庫(kù)連接或是網(wǎng)絡(luò)連接之類的資源昂灵,要確保你及時(shí)的清理這些資源避凝。如果你調(diào)用的API僅僅出發(fā)了無(wú)需檢查異常,你仍然需要在使用后主動(dòng)清理眨补。使用 塊管削。
類關(guān)閉 連接。這里的重點(diǎn)在于在 塊中關(guān)閉連接撑螺,無(wú)論是否出現(xiàn)了異常含思。
2.永遠(yuǎn)不要使用異常來(lái)控制流
生成棧追蹤的代價(jià)很昂貴,它的價(jià)值在于debug過(guò)程中使用。在一個(gè)流程控制中含潘,棧追蹤應(yīng)當(dāng)被忽視饲做,因?yàn)榭蛻舳酥幌胫廊绾芜M(jìn)行。
在下面的代碼中遏弱, 被用來(lái)進(jìn)行流程控制:
通過(guò)無(wú)限循環(huán)來(lái)增加計(jì)數(shù)艇炎,直到拋出異常。這種方式使得代碼難以閱讀腾窝,而且影響代碼性能缀踪。只在出現(xiàn)異常的場(chǎng)景拋出異常。
3.不要無(wú)視或是壓制異常
當(dāng)API的方法會(huì)拋出異常的時(shí)候虹脯,它在提醒你應(yīng)當(dāng)采取一些措施驴娃。如果需檢查異常沒(méi)有任何意義,那就干脆將其轉(zhuǎn)化為無(wú)需檢查異常再重新拋出循集。不要單純的用catch捕獲它然后繼續(xù)執(zhí)行唇敞,仿佛什么都沒(méi)有發(fā)生一樣。
4.不要捕獲最高層異常
繼承 的異常同樣是 的子類咒彤。捕獲 的同時(shí)疆柔,也捕獲了運(yùn)行時(shí)異常:
5.只記錄異常一次
將同一個(gè)異常多次記入日志會(huì)使得檢查追蹤棧的開(kāi)發(fā)人員感到困惑,不知道何處是報(bào)錯(cuò)的根源镶柱。所以只記錄一次旷档。
覺(jué)得本文對(duì)你有幫助?請(qǐng)分享給更多人歇拆。