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?我畫了一個簡單的類圖,並列出來典型例子,可以給你作爲參考,至少做到基本
心裏有數。

第二, 理解Java語言中操作Throwable的元素和實踐。掌握最基本的語法是必須的,如try-catch-fnally塊, throw、 throws關鍵字等。與此同時,也要懂得如何處理典型場景。
異常處理代碼比較繁瑣,比如我們需要寫很多千篇一律的捕獲代碼,或者在fnally裏面做一些資源回收工作。隨着Java語言的發展,引入了一些更加便利的特性,比如try-withresources和multiple catch,具體可以參考下面的代碼段。在編譯時期,會自動生成相應的處理邏輯,比如,自動按照約定俗成close那些擴展了AutoCloseable或者Closeable的對象。

try (BuferedReader br = new BuferedReader(…);
BuferedWriter writer = new BuferedWriter(…)) {// 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()的文檔,開頭就是“Prints this throwable and its backtrace to the standard error stream”。問題就在這裏,在稍微複雜一點的生產系統中,標準出錯(STERR)不是個合適的輸出選項,因爲你很難判斷出到底輸出到哪裏去了。
尤其是對於分佈式系統,如果發生異常,但是無法找到堆棧軌跡(stacktrace),這純屬是爲診斷設置障礙。所以,最好使用產品日誌,詳細地輸出到日誌系統裏。

我們接下來看下面的代碼段,體會一下Throw early, catch late原則。

public void readPreferences(String fleName){
//...perform operations...
InputStream in = new FileInputStream(fleName);
//...read the preferences fle...
}

如果fleName是null,那麼程序就會拋出NullPointerException,但是由於沒有第一時間暴露出問題,堆棧信息可能非常令人費解,往往需要相對複雜的定位。這個NPE只是作爲例子,實際產品代碼中,可能是各種情況,比如獲取配置失敗之類的。在發現問題的時候,第一時間拋出,能夠更加清晰地反映問題。我們可以修改一下,讓問題“throw early”,對應的異常信息就非常直觀了。

public void readPreferences(String flename) {
Objects. requireNonNull(flename);
//...perform other operations...
InputStream in = new FileInputStream(flename);
//...read the preferences fle...
}

至於“catch late”,其實是我們經常苦惱的問題,捕獲異常後,需要怎麼處理呢?最差的處理方式,就是我前面提到的“生吞異常”,本質上其實是掩蓋問題。如果實在不知道如何處理,可以選擇保留原有異常的cause信息,直接再拋出或者構建新的異常拋出去。在更高層面,因爲有了清晰的(業務)邏輯,往往會更清楚合適的處理方式是什麼。
有的時候,我們會根據需要自定義異常,這個時候除了保證提供足夠的信息,還有兩點需要考慮:

  • 1、是否需要定義成Checked Exception,因爲這種類型設計的初衷更是爲了從異常情況恢復,作爲異常設計者,我們往往有充足信息進行分類。
  • 2、在保證診斷信息足夠的同時,也要考慮避免包含敏感信息,因爲那樣可能導致潛在的安全問題。如果我們看Java的標準類庫,你可能注意到類似java.net.ConnectException,出錯信息是類似“ Connection refused (Connection refused)”,而不包含具體的機器名、 IP、端口等,一個重要考量就是信息安全。類似的情況在日誌中也有,比如,用戶數據一般是不可以輸出到日誌裏面的。

業界有一種爭論(甚至可以算是某種程度的共識), Java語言的Checked Exception也許是個設計錯誤,反對者列舉了幾點:

  • 1、Checked Exception的假設是我們捕獲了異常,然後恢復程序。但是,其實我們大多數情況下,根本就不可能恢復。 Checked Exception的使用,已經大大偏離了最初的設計目的。
  • 2、Checked Exception不兼容functional編程,如果你寫過Lambda/Stream代碼,相信深有體會。

很多開源項目,已經採納了這種實踐,比如Spring、 Hibernate等,甚至反映在新的編程語言設計中,比如Scala等。 如果有興趣,你可以參考:http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/。
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章