天天看點

Exception和Error有什麼差別

        Exception和Error都繼承了Throwable類,在java中隻有Throwable類型的執行個體才可以被抛出(throw)或者捕獲(catch),它是異常處理機制的基本組成類型。

        Exception和Error展現了java平台設計者對不同異常情況的分類。

        Exception是程式正常運作中,可以預料的意外情況,可能并且應該被捕獲,進行相應處理。

        Error是指在正常情況下,不大可能出現的情況,絕大部分Error都會導緻程式(比如JVM自身)處于非正常的、不可恢複的狀态。既然是非正常情況,是以不便于也不需要捕獲,常見的比如OutOfMemoryError之類,都是Error的子類。

        Exception又可分為可檢查異常和不檢查異常,可檢查異常在源代碼裡必須顯示地進行捕獲處理,這是編譯期檢查的一部分。前面我介紹的不可檢查的Error,是Throwable不是Exception。

        不檢查異常就是所謂的運作時異常,類似NullPointerException、ArrayIndexOutOfBoundsException之類,通常是可以編碼避免的邏輯錯誤,據圖根據需要來判斷是否需要捕獲,并不會在編譯期強制需求。

        前面談的大多是概念性的東西,下面來談一談實踐中的選擇。

先看第一段代碼:

try{

//業務代碼

//...

Thread.sleep(1000L);

}catch(Exception e){

//Ignore it

}

這段代碼雖然很短,但是已經違反了異常處理的兩個基本原則。

第一,盡量不要捕獲類似Exception這樣的通用異常,而是應該捕獲特定異常,在這裡是Thread.sleep()跑出的InterruptedException。

這是因為在日常的開發和合作中,我們讀代碼的機會往往超過寫代碼,軟體工程是門協作的藝術,是以我們有義務讓自己的代碼能夠直覺地展現出盡量多的資訊,而泛泛的Exception之類,恰恰隐藏了我們的目的。另外,我們也要保證程式不會捕獲到我們不希望捕獲的異常。比如,你可能更希望RuntimeException被擴散出來,而不是被捕獲。進一步講,除非深思熟慮了,否則不要捕獲Throwable或者Error,這樣很難保證我們能夠正确處理OutOfMemoryError.

第二,不要生吞異常。這是異常進行中要特别注意的事情,因為很可能會導緻非常難以診斷的詭異情況。

生吞異常,往往是基于假設這段代碼可能不會發生,或者感覺忽略異常時無所謂的,但是千萬不要在産品代碼做這種假設!

如果我們不把異常跑出來,或者也沒有輸出到日志之類,程式可能在後續代碼以不可控的方式結束。沒人能夠輕易判斷究竟是哪裡抛出了異常,以及是什麼原因産生了異常。

再來看看第二段代碼

try{

//業務代碼

//...

}catch(IOException e){

e.printStackTrace();

}

這段代碼作為一段實驗代碼,它是沒有任何問題的。但是在産品代碼中,通常都不允許這樣處理。

為什麼呢?

我們先來看看printStackTrace()的文檔,開頭就是“Prints this throwable and its backtrace to the standard error stream"。問題就在這裡,在稍微複雜一點的生産系統中,标準出錯不是個合适的輸出選擇,因為你很難判斷出到底輸出到哪裡去了。

尤其是對于分布式系統,如果發生異常,但是無法找到堆棧軌迹,這純屬是為診斷設定障礙。是以,最好使用産品日志,詳細的輸出到日志系統裡。

我們接下來看下面的代碼段,體會一下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 perferences file...

}

至于“catch late”。其實是我們經常苦惱的問題,捕獲異常後,需要怎麼處理呢?最差的處理方式,就是我前面提到的”生吞異常“,本質上其實是掩蓋問題。如果實在不知道如何處理,可以選擇保留原有異常的cause資訊,直接再抛出或者建構新的異常抛出去。在更高層面,因為有了清晰地(業務)邏輯,往往會更清楚合适的處理方式是什麼。

有的時候,我們會根據需要自定義異常,這個時候除了保證提供足夠的資訊,還有兩點需要考慮:

一、是否需要定義成Checked Exception,因為這種類型設計的初衷更是為了從異常情況恢複,作為異常設計者,我們往往有充           足資訊進行分類;

二、在保證診斷資訊足夠的同時,也要考慮避免包含敏感資訊,因為那樣可能導緻潛在的安全問題。如果我們看java類庫,你可         能注意到類似java.net.ConnectExcetion,出錯資訊是類似“Connection refused(Connection refused)",而不包含具體的機器         名、IP、端口号等,一個重要考量就是資訊安全。類似的情況在日志中也有,比如,使用者資料一般是不可以輸出到日志裡面         的。

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

一、Checked Exception的假設是我們捕獲了異常,然後恢複程式。但是,其實我們大多數情況下,根本就不可能恢複。                     Checked Exception的使用,已經大大偏離了最初的設計目的。

二、Checked Exception不相容functional程式設計,如果你寫過Lambda/Stream代碼,相信深有體會。

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

當然,很多人也覺得沒有必要矯枉過正,因為确實有一些異常,比如和環境相關的IO、網絡等,其實是存在可恢複性的,而且java已經通過業界的海量實踐,證明了其建構高品質軟體的能力。

我們從性能角度來審視一下java的異常處理機制,這裡有兩個可能會相對昂貴的地方:

一、try-catch 代碼段會産生額外的性能開銷,或者換個角度講,它往往會影響JVM對代碼進行優化,是以建議僅捕獲有必要的代         碼段,盡量不要一個大的try包裹整段的代碼;與此同時,利用異常控制代碼流程,也不是一個好主意,遠比我們通常意義上         的條件語句(if/else、switch)要低效。

二、java每執行個體化一個Exception,都會對當時的棧進行快照,這是一個相對比較重的操作。如果發生的比較頻繁,這個開銷就           不能被忽略了。

是以,對于部分追求極緻性能的底層類庫,有種方式是嘗試建立不進行棧快照的Exception。這本身也存在争議,因為這樣做的假設在于,我建立異常時知道未來是否需要堆棧。問題是,實際上可能嗎?小範圍或許可能,但是在大規模項目中,這麼做可能不是個理智的選擇。如果需要堆棧,但又沒有收集這些資訊,在複雜情況下,尤其是類似微服務這種分布式系統,這會大大增加診斷的難度。

當我們的服務出現反應變慢、吞吐量下降的時候,檢查發生最頻繁的Exception也是一種思路。