世界上存在永遠不會出錯的程序嗎?也許這只會出現(xiàn)在程序員的夢中乱顾。隨著編程語言和軟件的誕生板祝,異常情況就如影隨形地糾纏著我們,只有正確處理好意外情況走净,才能保證程序的可靠性券时。
Java語言在設(shè)計之初就提供了相對完善的異常處理機制,這也是Java得以大行其道的原因之一温技,因為這種機制大大降低了編寫和維護可靠程序的門檻革为。如今,異常處理機制已經(jīng)成為現(xiàn)代編程語言的標(biāo)配舵鳞。
今天我要問你的問題是震檩,請對比Exception和Error,另外蜓堕,運行時異常與一般異常有什么區(qū)別抛虏?
典型回答
Exception和Error都是繼承了Throwable類,在Java中只有Throwable類型的實例才可以被拋出(throw)或者捕獲(catch)套才,它是異常處理機制的基本組成類型迂猴。
Exception和Error體現(xiàn)了Java平臺設(shè)計者對不同異常情況的分類。Exception是程序正常運行中背伴,可以預(yù)料的意外情況沸毁,可能并且應(yīng)該被捕獲,進行相應(yīng)處理傻寂。
Error是指在正常情況下息尺,不大可能出現(xiàn)的情況,絕大部分的Error都會導(dǎo)致程序(比如JVM自身)處于非正常的疾掰、不可恢復(fù)狀態(tài)搂誉。既然是非正常情況,所以不便于也不需要捕獲静檬,常見的比如OutOfMemoryError之類炭懊,都是Error的子類并级。
Exception又分為可檢查(checked)異常和不檢查(unchecked)異常,可檢查異常在源代碼里必須顯式地進行捕獲處理侮腹,這是編譯期檢查的一部分嘲碧。前面我介紹的不可查的Error,是Throwable不是Exception凯旋。
不檢查異常就是所謂的運行時異常呀潭,類似 NullPointerException、ArrayIndexOutOfBoundsException之類至非,通常是可以編碼避免的邏輯錯誤钠署,具體根據(jù)需要來判斷是否需要捕獲,并不會在編譯期強制要求荒椭。
考點分析
分析Exception和Error的區(qū)別谐鼎,是從概念角度考察了Java處理機制∪せ荩總的來說狸棍,還處于理解的層面,面試者只要闡述清楚就好了味悄。
我們在日常編程中草戈,如何處理好異常是比較考驗功底的,我覺得需要掌握兩個方面侍瑟。
第一唐片,理解Throwable、Exception涨颜、Error的設(shè)計和分類费韭。比如,掌握那些應(yīng)用最為廣泛的子類庭瑰,以及如何自定義異常等星持。
很多面試官會進一步追問一些細節(jié),比如弹灭,你了解哪些Error督暂、Exception或者RuntimeException?我畫了一個簡單的類圖穷吮,并列出來典型例子逻翁,可以給你作為參考,至少做到基本心里有數(shù)酒来。
其中有些子類型,最好重點理解一下肪凛,比如NoClassDefFoundError和ClassNotFoundException有什么區(qū)別堰汉,這也是個經(jīng)典的入門題目辽社。
第二,理解Java語言中操作Throwable的元素和實踐翘鸭。掌握最基本的語法是必須的滴铅,如try-catch-finally塊,throw就乓、throws關(guān)鍵字等汉匙。與此同時,也要懂得如何處理典型場景生蚁。
異常處理代碼比較繁瑣噩翠,比如我們需要寫很多千篇一律的捕獲代碼,或者在finally里面做一些資源回收工作邦投。隨著Java語言的發(fā)展伤锚,引入了一些更加便利的特性,比如try-with-resources和multiple catch志衣,具體可以參考下面的代碼段屯援。在編譯時期,會自動生成相應(yīng)的處理邏輯念脯,比如狞洋,自動按照約定俗成close那些擴展了AutoCloseable或者Closeable的對象。
try (BufferedReader br = new BufferedReader(…);
? ? BufferedWriter writer = new BufferedWriter(…)) {// Try-with-resources
// do something
catch ( IOException | XEception e) {// Multiple catch
? // Handle it
}
知識擴展
前面談的大多是概念性的東西绿店,下面我來談些實踐中的選擇吉懊,我會結(jié)合一些代碼用例進行分析。
先開看第一個吧惯吕,下面的代碼反映了異常處理中哪些不當(dāng)之處惕它?
try {
? // 業(yè)務(wù)代碼
? // …
? Thread.sleep(1000L);
} catch (Exception e) {
? // Ignore it
}
這段代碼雖然很短,但是已經(jīng)違反了異常處理的兩個基本原則废登。
第一淹魄,盡量不要捕獲類似Exception這樣的通用異常,而是應(yīng)該捕獲特定異常堡距,在這里是Thread.sleep()拋出的InterruptedException甲锡。
這是因為在日常的開發(fā)和合作中,我們讀代碼的機會往往超過寫代碼羽戒,軟件工程是門協(xié)作的藝術(shù)缤沦,所以我們有義務(wù)讓自己的代碼能夠直觀地體現(xiàn)出盡量多的信息,而泛泛的Exception之類易稠,恰恰隱藏了我們的目的缸废。另外,我們也要保證程序不會捕獲到我們不希望捕獲的異常。比如企量,你可能更希望RuntimeException被擴散出來测萎,而不是被捕獲。
進一步講届巩,除非深思熟慮了硅瞧,否則不要捕獲Throwable或者Error,這樣很難保證我們能夠正確程序處理OutOfMemoryError恕汇。
第二腕唧,不要生吞(swallow)異常。這是異常處理中要特別注意的事情瘾英,因為很可能會導(dǎo)致非常難以診斷的詭異情況枣接。
生吞異常,往往是基于假設(shè)這段代碼可能不會發(fā)生方咆,或者感覺忽略異常是無所謂的月腋,但是千萬不要在產(chǎn)品代碼做這種假設(shè)!
如果我們不把異常拋出來瓣赂,或者也沒有輸出到日志(Logger)之類榆骚,程序可能在后續(xù)代碼以不可控的方式結(jié)束。沒人能夠輕易判斷究竟是哪里拋出了異常煌集,以及是什么原因產(chǎn)生了異常妓肢。
再來看看第二段代碼
try {
? // 業(yè)務(wù)代碼
? // …
} catch (IOException e) {
? ? e.printStackTrace();
}
這段代碼作為一段實驗代碼,它是沒有任何問題的苫纤,但是在產(chǎn)品代碼中碉钠,通常都不允許這樣處理。你先思考一下這是為什么呢卷拘?
我們先來看看printStackTrace()的文檔喊废,開頭就是“Prints this throwable and its backtrace to the?standard error stream”。問題就在這里栗弟,在稍微復(fù)雜一點的生產(chǎn)系統(tǒng)中污筷,標(biāo)準(zhǔn)出錯(STERR)不是個合適的輸出選項,因為你很難判斷出到底輸出到哪里去了乍赫。
尤其是對于分布式系統(tǒng)瓣蛀,如果發(fā)生異常,但是無法找到堆棧軌跡(stacktrace)雷厂,這純屬是為診斷設(shè)置障礙惋增。所以,最好使用產(chǎn)品日志改鲫,詳細地輸出到日志系統(tǒng)里诈皿。
我們接下來看下面的代碼段林束,體會一下Throw early, catch late原則。
public void readPreferences(String fileName){
//...perform operations...
InputStream in = new FileInputStream(fileName);
//...read the preferences file...
}
如果fileName是null稽亏,那么程序就會拋出NullPointerException诊县,但是由于沒有第一時間暴露出問題,堆棧信息可能非常令人費解措左,往往需要相對復(fù)雜的定位。這個NPE只是作為例子避除,實際產(chǎn)品代碼中怎披,可能是各種情況,比如獲取配置失敗之類的瓶摆。在發(fā)現(xiàn)問題的時候凉逛,第一時間拋出,能夠更加清晰地反映問題群井。
我們可以修改一下状飞,讓問題“throw early”,對應(yīng)的異常信息就非常直觀了书斜。
public void readPreferences(String filename) {
Objects. requireNonNull(filename);
//...perform other operations...
InputStream in = new FileInputStream(filename);
//...read the preferences file...
}
至于“catch late”诬辈,其實是我們經(jīng)常苦惱的問題荐吉,捕獲異常后焙糟,需要怎么處理呢?最差的處理方式样屠,就是我前面提到的“生吞異炒┐椋”,本質(zhì)上其實是掩蓋問題痪欲。如果實在不知道如何處理悦穿,可以選擇保留原有異常的cause信息,直接再拋出或者構(gòu)建新的異常拋出去业踢。在更高層面栗柒,因為有了清晰的(業(yè)務(wù))邏輯,往往會更清楚合適的處理方式是什么陨亡。
有的時候傍衡,我們會根據(jù)需要自定義異常,這個時候除了保證提供足夠的信息负蠕,還有兩點需要考慮:
是否需要定義成Checked Exception蛙埂,因為這種類型設(shè)計的初衷更是為了從異常情況恢復(fù),作為異常設(shè)計者遮糖,我們往往有充足信息進行分類绣的。
在保證診斷信息足夠的同時,也要考慮避免包含敏感信息,因為那樣可能導(dǎo)致潛在的安全問題屡江。如果我們看Java的標(biāo)準(zhǔn)類庫芭概,你可能注意到類似java.net.ConnectException,出錯信息是類似“ Connection refused (Connection refused)”惩嘉,而不包含具體的機器名罢洲、IP、端口等文黎,一個重要考量就是信息安全惹苗。類似的情況在日志中也有,比如耸峭,用戶數(shù)據(jù)一般是不可以輸出到日志里面的桩蓉。
業(yè)界有一種爭論(甚至可以算是某種程度的共識),Java語言的Checked Exception也許是個設(shè)計錯誤劳闹,反對者列舉了幾點:
Checked Exception的假設(shè)是我們捕獲了異常院究,然后恢復(fù)程序。但是本涕,其實我們大多數(shù)情況下业汰,根本就不可能恢復(fù)。Checked Exception的使用菩颖,已經(jīng)大大偏離了最初的設(shè)計目的蔬胯。
Checked Exception不兼容functional編程,如果你寫過Lambda/Stream代碼位他,相信深有體會氛濒。
很多開源項目,已經(jīng)采納了這種實踐鹅髓,比如Spring舞竿、Hibernate等,甚至反映在新的編程語言設(shè)計中窿冯,比如Scala等骗奖。 如果有興趣,你可以參考:
http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/醒串。
當(dāng)然执桌,很多人也覺得沒有必要矯枉過正,因為確實有一些異常芜赌,比如和環(huán)境相關(guān)的IO仰挣、網(wǎng)絡(luò)等,其實是存在可恢復(fù)性的缠沈,而且Java已經(jīng)通過業(yè)界的海量實踐膘壶,證明了其構(gòu)建高質(zhì)量軟件的能力错蝴。我就不再進一步解讀了,感興趣的同學(xué)可以點擊鏈接颓芭,觀看Bruce Eckel在2018年全球軟件開發(fā)大會QCon的分享Failing at Failing: How and Why We’ve Been Nonchalantly Moving Away From Exception Handling顷锰。
我們從性能角度來審視一下Java的異常處理機制,這里有兩個可能會相對昂貴的地方:
try-catch代碼段會產(chǎn)生額外的性能開銷亡问,或者換個角度說官紫,它往往會影響JVM對代碼進行優(yōu)化,所以建議僅捕獲有必要的代碼段州藕,盡量不要一個大的try包住整段的代碼万矾;與此同時,利用異成骺颍控制代碼流程,也不是一個好主意后添,遠比我們通常意義上的條件語句(if/else笨枯、switch)要低效。
Java每實例化一個Exception遇西,都會對當(dāng)時的棧進行快照馅精,這是一個相對比較重的操作。如果發(fā)生的非常頻繁粱檀,這個開銷可就不能被忽略了洲敢。
所以,對于部分追求極致性能的底層類庫茄蚯,有種方式是嘗試創(chuàng)建不進行椦古恚快照的Exception。這本身也存在爭議渗常,因為這樣做的假設(shè)在于壮不,我創(chuàng)建異常時知道未來是否需要堆棧。問題是皱碘,實際上可能嗎询一?小范圍或許可能,但是在大規(guī)模項目中癌椿,這么做可能不是個理智的選擇健蕊。如果需要堆棧,但又沒有收集這些信息踢俄,在復(fù)雜情況下缩功,尤其是類似微服務(wù)這種分布式系統(tǒng),這會大大增加診斷的難度都办。
當(dāng)我們的服務(wù)出現(xiàn)反應(yīng)變慢掂之、吞吐量下降的時候抗俄,檢查發(fā)生最頻繁的Exception也是一種思路。關(guān)于診斷后臺變慢的問題世舰,我會在后面的Java性能基礎(chǔ)模塊中系統(tǒng)探討动雹。
今天,我從一個常見的異常處理概念問題跟压,簡單總結(jié)了Java異常處理的機制胰蝠。并結(jié)合代碼,分析了一些普遍認可的最佳實踐震蒋,以及業(yè)界最新的一些異常使用共識茸塞。最后,我分析了異常性能開銷查剖,希望對你有所幫助钾虐。