Exception和Error有什麼區別

之前寫了一個基礎知識的文章,裏邊已經介紹了Exception和Error的區別,也介紹了異
常的使用規則,但是沒有具體說一下在日常使用中的一個規範,有時,面試時會問到的一些
點。 本篇文章稍微梳理一下。

在日常代碼編寫過程中,肯定不會出現沒有錯誤的程序,無錯誤的程序有可能會出現在“碼農”的幻想中。我們在日常的開發中,異常的處理肯定是大家經常碰到的,但是碰到的時候,咱們是否能正確的把異常cath到,然後是否正確的把異常給處理了。 只有正確的處理好異常,才能保證咱們的程序的健壯性。


基本概念

首先Exception和Error都是繼承於Throwable 類,在 Java 中只有 Throwable 類型的實例纔可以被拋出(throw)或者捕獲(catch),它是異常處理機制的基本組成類型。

Exception和Error體現了JAVA這門語言對於異常處理的兩種方式。

Exception是java程序運行中可預料的異常情況,咱們可以獲取到這種異常,並且對這種異常進行業務外的處理。

Error是java程序運行中不可預料的異常情況,這種異常發生以後,會直接導致JVM不可處理或者不可恢復的情況。所以這種異常不可能抓取到,比如OutOfMemoryError、NoClassDefFoundError等。

其中的Exception又分爲檢查性異常非檢查性異常。兩個根本的區別在於,檢查性異常 必須在編寫代碼時,使用try catch捕獲(比如:IOException異常)。非檢查性異常 在代碼編寫使,可以忽略捕獲操作(比如:ArrayIndexOutOfBoundsException),這種異常是在代碼編寫或者使用過程中通過規範可以避免發生的。 切記,Error是Throw不是Exception

使用場景

以上的分析 Exception 和 Error 的區別,是從概念角度考察了 Java 處理機制。總的來說,還處於理解的層面,面試者只要闡述清楚就好了。

我們在日常編程中,如何處理好異常是比較考驗功底的,我覺得需要掌握兩個方面

第一,理解 Throwable、Exception、Error 的設計和分類。比如,掌握那些應用最爲廣泛的子類,以及如何自定義異常等。

一般在面試的情況下,都會問你,你知道Error和Exception下的子類有哪些?能說一下他們具體的應用場景?

Throwable簡單的架構圖

其中有一個比較經典的面試題目, 就是 NoClassDefFoundError 和 ClassNotFoundException 有什麼區別

區別一: NoClassDefFoundError它是ErrorClassNotFoundExceptionException。

區別二:還有一個區別在於NoClassDefFoundErrorJVM運行時通過classpath加載類
時,找不到對應的類而拋出的錯誤。ClassNotFoundException是在編譯過程中如果可能出現此異常,在編譯過程中必須將ClassNotFoundException異常拋出!

NoClassDefFoundError發生場景如下:
    1、類依賴的class或者jar不存在 (簡單說就是maven生成運行包後被篡改)
    2、類文件存在,但是存在不同的域中 (簡單說就是引入的類不在對應的包下)
    3、大小寫問題,javac編譯的時候是無視大小的,很有可能你編譯出來的class文件就與想要的不一樣!這個沒有做驗證


    ClassNotFoundException發生場景如下:
    1、調用classforName方法時,找不到指定的類
    2ClassLoader 中的 findSystemClass() 方法時,找不到指定的類

舉例說明如下:
    Class.forName("abc"); 比如abc這個類不存項目中,代碼編寫時,就會提示此異常是檢查性異常,比如將此異常拋出。

第二,理解 Java 語言中操作 Throwable 的元素和實踐。掌握最基本的語法是必須的,如 try-catch-finally 塊,throw、throws 關鍵字等。與此同時,也要懂得如何處理典型場景。

throw是存在於方法的代碼塊中,而throws是存在於方法外圍,一般是在方法名後邊 throws XXXException;

有個重要的點需要記住, 就是try-catch-finally中rerun的執行順序問題


try{
    retrun 3;
}catch{
    e.printStackTrace();
}finally{
    return 4;
}

//上邊情況下,實際返回的是4;

try{
    int x = 3;
    retrun x;
}catch{
    e.printStackTrace();
}finally{
    x++;
}

//上邊情況下,實際返回的3;

這是爲什麼呢? 因爲finally的業務操作是在try業務操作的return返回調用者者之前執行。按照剛纔第一種情況,實際情況是,執行完try中的業務邏輯就,return返回的操作會先存儲到一個臨時的堆棧中,此時不給調用者返回,隨後執行finally中的業務代碼。如果finally中有return操作,那麼就會把finally中的return值與try中的return值進行替換。隨後將最終數據返回給調用者。

知識擴展
前面談的大多是概念性的東西,下面我來談些實踐中的選擇,我會結合一些代碼用例進行分析。

先開看第一個吧,下面的代碼反映了異常處理中哪些不當之處?

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();
}

這段代碼作爲一段實驗代碼,它是沒有任何問題的,但是在產品代碼中,通常都不允許這樣處理。你先思考一下這是爲什麼呢?

這樣的代碼在代碼規範中是沒有問題的,他的問題出在,異常中的異常日誌如何輸出的問題。按照上邊的輸出,如果實在複雜的系統中,會判斷不出來,異常具體在哪裏打印出來的。
尤其是對於分佈式系統,如果發生異常,但是無法找到堆棧軌跡(stacktrace),這純屬是爲診斷設置障礙。所以,最好使用產品日誌,詳細地輸出到日誌系統裏

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

//正常業務代碼
public void readPreferences(String fileName){
     //...perform operations... 
    InputStream in = new FileInputStream(fileName);
     //...read the preferences file...
}

//“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 信息,直接再拋出或者構建新的異常拋出去。在更高層面,因爲有了清晰的(業務)邏輯,往往會更清楚合適的處理方式是什麼。
     還有一種處理方式,可自定義異常,將業務異常轉換爲業務術語,但是拋出異常時,必須把異常的cause信息打印出,方便跟蹤問題,在最短的時間內,解決問題。但是需要考慮兩點

  1. 自定異常時,需要考慮自定義異常是否爲檢查性異常,因爲這種類型設計的初衷更是爲了從異常情況恢復,作爲異常設計者,我們往往有充足信息進行分類。

  2. 在保證診斷信息足夠的同時,也要考慮避免包含敏感信息,因爲那樣可能導致潛在的安全問題。如果我們看 Java 的標準類庫,你可能注意到類似 java.net.ConnectException,出錯信息是類似“ Connection refused (Connection refused)”,而不包含具體的機器名、IP、端口等,一個重要考量就是信息安全。類似的情況在日誌中也有,比如,用戶數據一般是不可以輸出到日誌裏面的。


對於異常中的檢查性異常,我們簡單的說一下。目前檢查性異常被業界說是java的一種設計缺陷。有一下幾點可以參考一下

     Checked Exception 的假設是我們捕獲了異常,然後恢復程序。但是,其實我們大多數情況下,根本就不可能恢復。Checked Exception 的使用,已經大大偏離了最初的設計目的。
     當然,很多人也覺得沒有必要矯枉過正,因爲確實有一些異常,比如和環境相關的 IO、網絡等,其實是存在可恢復性的,而且 Java 已經通過業界的海量實踐,證明了其構建高質量軟件的能力。


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

  1. try-catch 代碼段會產生額外的性能開銷,或者換個角度說,它往往會影響 JVM 對代碼進行優化,所以建議僅捕獲有必要的代碼段,儘量不要一個大的 try 包住整段的代碼;與此同時,利用異常控制代碼流程,也不是一個好主意,遠比我們通常意義上的條件語句(if/else、switch)要低效。
  2. java 每實例化一個 Exception,都會對當時的棧進行快照,這是一個相對比較重的操作。如果發生的非常頻繁,這個開銷可就不能被忽略了。

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

     當我們的服務出現反應變慢、吞吐量下降的時候,檢查發生最頻繁的 Exception 也是一種思路。關於診斷後臺變慢的問題,我會在後面的 Java 性能基礎模塊中系統探討。


以上介紹的就是異常的基本知識,和日常編碼中的見解。但是隨着 微服務 概念的日漸成熟,異常在微服務中的處理方式,也不能像普通項目。
當執行異常時,保存當前任務信息加入重試隊列。重試的策略根據業務需要決定,當達到重試上限依然無法成功,記錄任務執行失敗,同時發出告警。
同時需要將微服務中的業務異常日誌,進行記錄,這種記錄一般會跟隨着唯一標識,方便在微服務項目中,定位問題。


本文參考了極客時間中楊曉峯老師發表的java核心技術36講。

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