世界上存在永遠不會出錯的程序嗎?也許這只會出現在程序員的夢中。隨著編程語言和軟件的誕生,異常情況就如影隨形地糾纏著我們,只有正確處理好意外情況,才能保證程序的可靠性。
Java 語言在設計之初就提供了相對完善的異常處理機制,這也是 Java 得以大行其道的原因之一,因為這種機制大大降低了編寫和維護可靠程序的門檻。如今,異常處理機制已經成為現代編程語言的標配。
今天我要問你的問題是,請對比 Exception 和 Error,另外,運行時異常與一般異常有什么區別?
## 典型回答
Exception 和 Error 都是繼承了 Throwable 類,在 Java 中只有 Throwable 類型的實例才可以被拋出(throw)或者捕獲(catch),它是異常處理機制的基本組成類型。
Exception 和 Error 體現了 Java 平臺設計者對不同異常情況的分類。Exception 是程序正常運行中,可以預料的意外情況,可能并且應該被捕獲,進行相應處理。
Error 是指在正常情況下,不大可能出現的情況,絕大部分的 Error 都會導致程序(比如 JVM 自身)處于非正常的、不可恢復狀態。既然是非正常情況,所以不便于也不需要捕獲,常見的比如 OutOfMemoryError 之類,都是 Error 的子類。
Exception 又分為**可檢查**(checked)異常和**不檢查**(unchecked)異常,可檢查異常在源代碼里必須顯式地進行捕獲處理,這是編譯期檢查的一部分。前面我介紹的不可查的 Error,是 Throwable 不是 Exception。
不檢查異常就是所謂的運行時異常,類似 NullPointerException、ArrayIndexOutOfBoundsException 之類,通常是可以編碼避免的邏輯錯誤,具體根據需要來判斷是否需要捕獲,并不會在編譯期強制要求。
## 考點分析
分析 Exception 和 Error 的區別,是從概念角度考察了 Java 處理機制。總的來說,還處于理解的層面,面試者只要闡述清楚就好了。
我們在日常編程中,如何處理好異常是比較考驗功底的,我覺得需要掌握兩個方面。
第一,**理解 Throwable、Exception、Error 的設計和分類**。比如,掌握那些應用最為廣泛的子類,以及如何自定義異常等。
很多面試官會進一步追問一些細節,比如,你了解哪些 Error、Exception 或者 RuntimeException?我畫了一個簡單的類圖,并列出來典型例子,可以給你作為參考,至少做到基本心里有數。

其中有些子類型,最好重點理解一下,比如 NoClassDefFoundError 和 ClassNotFoundException 有什么區別,這也是個經典的入門題目。
第二,**理解 Java 語言中操作 Throwable 的元素和實踐**。掌握最基本的語法是必須的,如 try-catch-finally 塊,throw、throws 關鍵字等。與此同時,也要懂得如何處理典型場景。
異常處理代碼比較繁瑣,比如我們需要寫很多千篇一律的捕獲代碼,或者在 finally 里面做一些資源回收工作。隨著 Java 語言的發展,引入了一些更加便利的特性,比如 try-with-resources 和 multiple catch,具體可以參考下面的代碼段。在編譯時期,會自動生成相應的處理邏輯,比如,自動按照約定俗成 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
}
~~~
## 知識擴展
前面談的大多是概念性的東西,下面我來談些實踐中的選擇,我會結合一些代碼用例進行分析。
先開看第一個吧,下面的代碼反映了異常處理中哪些不當之處?
~~~
try {
// 業務代碼
// …
Thread.sleep(1000L);
} catch (Exception e) {
// Ignore it
}
~~~
這段代碼雖然很短,但是已經違反了異常處理的兩個基本原則。
第一,**盡量不要捕獲類似 Exception 這樣的通用異常,而是應該捕獲特定異常**,在這里是 Thread.sleep() 拋出的 InterruptedException。
這是因為在日常的開發和合作中,我們讀代碼的機會往往超過寫代碼,軟件工程是門協作的藝術,所以我們有義務讓自己的代碼能夠直觀地體現出盡量多的信息,而泛泛的 Exception 之類,恰恰隱藏了我們的目的。另外,我們也要保證程序不會捕獲到我們不希望捕獲的異常。比如,你可能更希望 RuntimeException 被擴散出來,而不是被捕獲。
進一步講,除非深思熟慮了,否則不要捕獲 Throwable 或者 Error,這樣很難保證我們能夠正確程序處理 OutOfMemoryError。
第二,**不要生吞(swallow)異常**。這是異常處理中要特別注意的事情,因為很可能會導致非常難以診斷的詭異情況。
生吞異常,往往是基于假設這段代碼可能不會發生,或者感覺忽略異常是無所謂的,但是千萬不要在產品代碼做這種假設!
如果我們不把異常拋出來,或者也沒有輸出到日志(Logger)之類,程序可能在后續代碼以不可控的方式結束。沒人能夠輕易判斷究竟是哪里拋出了異常,以及是什么原因產生了異常。
再來看看第二段代碼
~~~
try {
// 業務代碼
// …
} catch (IOException e) {
e.printStackTrace();
}
~~~
這段代碼作為一段實驗代碼,它是沒有任何問題的,但是在產品代碼中,通常都不允許這樣處理。你先思考一下這是為什么呢?
我們先來看看[printStackTrace()](https://docs.oracle.com/javase/9/docs/api/java/lang/Throwable.html#printStackTrace--)的文檔,開頭就是“Prints this throwable and its backtrace to the**standard error stream**”。問題就在這里,在稍微復雜一點的生產系統中,標準出錯(STERR)不是個合適的輸出選項,因為你很難判斷出到底輸出到哪里去了。
尤其是對于分布式系統,如果發生異常,但是無法找到堆棧軌跡(stacktrace),這純屬是為診斷設置障礙。所以,最好使用產品日志,詳細地輸出到日志系統里。
我們接下來看下面的代碼段,體會一下**Throw early, catch late 原則**。
~~~
public void readPreferences(String fileName){
//...perform operations...
InputStream in = new FileInputStream(fileName);
//...read the preferences file...
}
~~~
如果 fileName 是 null,那么程序就會拋出 NullPointerException,但是由于沒有第一時間暴露出問題,堆棧信息可能非常令人費解,往往需要相對復雜的定位。這個 NPE 只是作為例子,實際產品代碼中,可能是各種情況,比如獲取配置失敗之類的。在發現問題的時候,第一時間拋出,能夠更加清晰地反映問題。
我們可以修改一下,讓問題“throw early”,對應的異常信息就非常直觀了。
~~~
public void readPreferences(String filename) {
Objects. requireNonNull(filename);
//...perform other operations...
InputStream in = new FileInputStream(filename);
//...read the preferences file...
}
~~~
至于“catch late”,其實是我們經常苦惱的問題,捕獲異常后,需要怎么處理呢?最差的處理方式,就是我前面提到的“生吞異常”,本質上其實是掩蓋問題。如果實在不知道如何處理,可以選擇保留原有異常的 cause 信息,直接再拋出或者構建新的異常拋出去。在更高層面,因為有了清晰的(業務)邏輯,往往會更清楚合適的處理方式是什么。
有的時候,我們會根據需要自定義異常,這個時候除了保證提供足夠的信息,還有兩點需要考慮:
* 是否需要定義成 Checked Exception,因為這種類型設計的初衷更是為了從異常情況恢復,作為異常設計者,我們往往有充足信息進行分類。
* 在保證診斷信息足夠的同時,也要考慮避免包含敏感信息,因為那樣可能導致潛在的安全問題。如果我們看 Java 的標準類庫,你可能注意到類似 java.net.ConnectException,出錯信息是類似“ Connection refused (Connection refused)”,而不包含具體的機器名、IP、端口等,一個重要考量就是信息安全。類似的情況在日志中也有,比如,用戶數據一般是不可以輸出到日志里面的。
業界有一種爭論(甚至可以算是某種程度的共識),Java 語言的 Checked Exception 也許是個設計錯誤,反對者列舉了幾點:
* Checked Exception 的假設是我們捕獲了異常,然后恢復程序。但是,其實我們大多數情況下,根本就不可能恢復。Checked Exception 的使用,已經大大偏離了最初的設計目的。
* Checked Exception 不兼容 functional 編程,如果你寫過 Lambda/Stream 代碼,相信深有體會。
很多開源項目,已經采納了這種實踐,比如 Spring、Hibernate 等,甚至反映在新的編程語言設計中,比如 Scala 等。 如果有興趣,你可以參考:
[http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/](http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/)。
當然,很多人也覺得沒有必要矯枉過正,因為確實有一些異常,比如和環境相關的 IO、網絡等,其實是存在可恢復性的,而且 Java 已經通過業界的海量實踐,證明了其構建高質量軟件的能力。我就不再進一步解讀了,感興趣的同學可以點擊**[鏈接](http://v.qq.com/x/page/d0635rf5x0o.html)**,觀看 Bruce Eckel 在 2018 年全球軟件開發大會 QCon 的分享 Failing at Failing: How and Why We’ve Been Nonchalantly Moving Away From Exception Handling。
我們從性能角度來審視一下 Java 的異常處理機制,這里有兩個可能會相對昂貴的地方:
* try-catch 代碼段會產生額外的性能開銷,或者換個角度說,它往往會影響 JVM 對代碼進行優化,所以建議僅捕獲有必要的代碼段,盡量不要一個大的 try 包住整段的代碼;與此同時,利用異常控制代碼流程,也不是一個好主意,遠比我們通常意義上的條件語句(if/else、switch)要低效。
* Java 每實例化一個 Exception,都會對當時的棧進行快照,這是一個相對比較重的操作。如果發生的非常頻繁,這個開銷可就不能被忽略了。
所以,對于部分追求極致性能的底層類庫,有種方式是嘗試創建不進行棧快照的 Exception。這本身也存在爭議,因為這樣做的假設在于,我創建異常時知道未來是否需要堆棧。問題是,實際上可能嗎?小范圍或許可能,但是在大規模項目中,這么做可能不是個理智的選擇。如果需要堆棧,但又沒有收集這些信息,在復雜情況下,尤其是類似微服務這種分布式系統,這會大大增加診斷的難度。
當我們的服務出現反應變慢、吞吐量下降的時候,檢查發生最頻繁的 Exception 也是一種思路。關于診斷后臺變慢的問題,我會在后面的 Java 性能基礎模塊中系統探討。
今天,我從一個常見的異常處理概念問題,簡單總結了 Java 異常處理的機制。并結合代碼,分析了一些普遍認可的最佳實踐,以及業界最新的一些異常使用共識。最后,我分析了異常性能開銷,希望對你有所幫助。
## 一課一練
關于今天我們討論的題目你做到心中有數了嗎?可以思考一個問題,對于異常處理編程,不同的編程范式也會影響到異常處理策略,比如,現在非常火熱的反應式編程(Reactive Stream),因為其本身是異步、基于事件機制的,所以出現異常情況,決不能簡單拋出去;另外,由于代碼堆棧不再是同步調用那種垂直的結構,這里的異常處理和日志需要更加小心,我們看到的往往是特定 executor 的堆棧,而不是業務方法調用關系。對于這種情況,你有什么好的辦法嗎?
- 前言
- 開篇詞
- 開篇詞 -以面試題為切入點,有效提升你的Java內功
- 模塊一 Java基礎
- 第1講 談談你對Java平臺的理解?
- 第2講 Exception和Error有什么區別?
- 第3講 談談final、finally、 finalize有什么不同?
- 第4講 強引用、軟引用、弱引用、幻象引用有什么區別?
- 第5講 String、StringBuffer、StringBuilder有什么區別?
- 第6講 動態代理是基于什么原理?
- 第7講 int和Integer有什么區別?
- 第8講 對比Vector、ArrayList、LinkedList有何區別?
- 第9講 對比Hashtable、HashMap、TreeMap有什么不同?
- 第10講 如何保證集合是線程安全的? ConcurrentHashMap如何實現高效地線程安全?
- 第11講 Java提供了哪些IO方式? NIO如何實現多路復用?
- 第12講 Java有幾種文件拷貝方式?哪一種最高效?
- 第13講 談談接口和抽象類有什么區別?
- 第14講 談談你知道的設計模式?
- 模塊二 Java進階
- 第15講 synchronized和ReentrantLock有什么區別呢?
- 第16講 synchronized底層如何實現?什么是鎖的升級、降級?
- 第17講 一個線程兩次調用start()方法會出現什么情況?
- 第18講 什么情況下Java程序會產生死鎖?如何定位、修復?
- 第19講 Java并發包提供了哪些并發工具類?
- 第20講 并發包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么區別?
- 第21講 Java并發類庫提供的線程池有哪幾種? 分別有什么特點?
- 第22講 AtomicInteger底層實現原理是什么?如何在自己的產品代碼中應用CAS操作?
- 第23講 請介紹類加載過程,什么是雙親委派模型?
- 第24講 有哪些方法可以在運行時動態生成一個Java類?
- 第25講 談談JVM內存區域的劃分,哪些區域可能發生OutOfMemoryError?
- 第26講 如何監控和診斷JVM堆內和堆外內存使用?
- 第27講 Java常見的垃圾收集器有哪些?
- 第28講 談談你的GC調優思路?
- 第29講 Java內存模型中的happen-before是什么?
- 第30講 Java程序運行在Docker等容器環境有哪些新問題?
- 模塊三 Java安全基礎
- 第31講 你了解Java應用開發中的注入攻擊嗎?
- 第32講 如何寫出安全的Java代碼?
- 模塊四 Java性能基礎
- 第33講 后臺服務出現明顯“變慢”,談談你的診斷思路?
- 第34講 有人說“Lambda能讓Java程序慢30倍”,你怎么看?
- 第35講 JVM優化Java代碼時都做了什么?
- 模塊五 Java應用開發擴展
- 第36講 談談MySQL支持的事務隔離級別,以及悲觀鎖和樂觀鎖的原理和應用場景?
- 第37講 談談Spring Bean的生命周期和作用域?
- 第38講 對比Java標準NIO類庫,你知道Netty是如何實現更高性能的嗎?
- 第39講 談談常用的分布式ID的設計方案?Snowflake是否受冬令時切換影響?
- 周末福利
- 周末福利 談談我對Java學習和面試的看法
- 周末福利 一份Java工程師必讀書單
- 結束語
- 結束語 技術沒有終點
- 結課測試 Java核心技術的這些知識,你真的掌握了嗎?