Java——Exception

gary-liu發表於2016-10-04

異常情形是指阻止當前方法或者作用域繼續執行的問題。比如使用者輸入了非法資料、要開啟的檔案不存在、網路通訊時連線中斷,或者JVM記憶體溢位等都可能導致異常。通過後面的異常型別和常見異常的介紹,可以知道產生的異常的原因有很多,有可能是使用者不當的操作或者程式中的邏輯錯誤,也有可能是JVM等物理錯誤產生的。

異常型別

檢查性異常

最具代表的檢查性異常是使用者錯誤或問題引起的異常,這是程式設計師無法預見的。例如要開啟一個不存在檔案時,一個異常就發生了,這些異常在編譯時不能被簡單地忽略。常見的檢查性異常有ClassNotFoundException,CloneNotSupportedException,IllegalAccessException,InstantiationException,InterruptedException,NoSuchFieldException,NoSuchMethodException等。

執行時異常

執行時異常是可能被程式設計師避免的異常。與檢查性異常相反,執行時異常可以在編譯時被忽略。這些異常一般是由程式邏輯錯誤引起的,程式應該從邏輯角度儘可能避免這類異常的發生。該類異常大多是RuntimeException類及其子類異常,比如ArithmeticException,ArrayIndexOutOfBoundsException,ClassCastException,IllegalArgumentException,IndexOutOfBoundsException,NullPointerException,NumberFormatException,StringIndexOutOfBoundsException等。

錯誤

錯誤不是異常,而是脫離程式設計師控制的問題。錯誤在程式碼中通常被忽略。例如棧溢位,JVM記憶體溢位,Java虛擬機器執行錯誤(Virtual MachineError)、類定義錯誤(NoClassDefFoundError)等,它們在編譯時也檢查不到的。

異常類

Throwable是java語言中所有錯誤和異常的超類,它有兩個子類:Error、Exception。

Error

其中Error為錯誤,是程式無法處理的,如OutOfMemoryError、ThreadDeath等,出現這種情況你唯一能做的就是聽之任之,交由JVM來處理,不過JVM在大多數情況下會選擇終止執行緒。

Exception

而Exception是程式可以處理的異常。它又分為兩種:CheckedException(檢查性異常)和UncheckedException(非檢查性異常)。其中CheckException發生在編譯階段,必須要使用try…catch或者throws子句宣告丟擲,否則編譯不通過。而UncheckedException發生在執行期,具有不確定性,主要是由於程式的邏輯問題所引起的。

異常和錯誤的區別:異常能被程式本身可以處理,錯誤是無法處理的。

異常類繼承關係圖如下
這裡寫圖片描述
圖中紅色部分為受檢查異常,它們必須被捕獲,或者在函式中宣告為丟擲該異常。圖片來自: http://www.importnew.com/11725.html

異常處理機制

在 Java 應用程式中,異常處理機制為:丟擲異常,捕捉異常。

捕獲異常

在方法丟擲異常之後,執行時系統將轉為尋找合適的異常處理器(exception handler)。潛在的異常處理器是異常發生時依次存留在呼叫棧中的方法的集合。當異常處理器所能處理的異常型別與方法丟擲的異常型別相符時,即為合適 的異常處理器。執行時系統從發生異常的方法開始,依次回查呼叫棧中的方法,直至找到含有合適異常處理器的方法並執行。當執行時系統遍歷呼叫棧而未找到合適 的異常處理器,則執行時系統終止。同時,意味著Java程式的終止。

捕捉異常通過try-catch語句或者try-catch-finally語句實現。如果有多個catch子句,應該儘量將捕獲底層異常類的catch子句放在前面,同時儘量將捕獲相對高層的異常類的catch子句放在後面,否則,捕獲底層異常類的catch子句將可能會被遮蔽。一旦某個catch捕獲到匹配的異常型別,將進入異常處理程式碼。一經處理結束,就意味著整個try-catch語句結束,其他的catch子句不再有匹配和捕獲異常型別的機會。

try 塊:用於捕獲異常。其後可接零個或多個catch塊,如果沒有catch塊,則必須跟一個finally塊。

finally 塊:無論是否捕獲或處理異常,finally塊裡的語句都會被執行。當在try塊或catch塊中遇到return語句時,finally語句塊將在方法返回之前被執行。在以下4種特殊情況下,finally塊不會被執行:

  1. 在finally語句塊中發生了異常

  2. 在前面的程式碼中用了System.exit()退出程式

  3. 程式所在的執行緒死亡

  4. 關閉CPU

對捕獲的異常如何處理:

  1. 處理異常。對所發生的的異常進行一番處理,如修正錯誤、提醒、寫入日誌等

  2. 重新丟擲異常。直接上拋異常,給呼叫方處理

  3. 封裝異常。自定義異常,對原來的異常資訊進行分類,然後進行封裝處理

丟擲異常

當一個方法出現錯誤引發異常時,方法建立異常物件並交付執行時系統,異常物件中包含了異常型別和異常出現時的程式狀態等異常資訊,執行時系統負責尋找處置異常的程式碼並執行。丟擲異常可以使用throw和throws實現。

  1. throws是方法丟擲異常。在方法宣告中,如果新增了throws子句,表示該方法即將丟擲異常,異常的處理交由它的呼叫者。呼叫方法必須遵循任何可檢查性異常的處理和宣告規則,比如若覆蓋一個方法,則不能宣告與覆蓋方法不同的異常,宣告的任何異常必須是被覆蓋方法所宣告異常的同類或子類。

  2. throw是語句丟擲異常。它不可以單獨使用,要麼與try/catch一起使用,要麼與throws一起使用。

異常鏈

Java 這種通過 throws 向上傳遞異常資訊的處理機制就會形成異常鏈。在異常鏈的使用中,throw 丟擲的是一個新的異常資訊,這樣勢必會導致原有的異常資訊丟失,如何保持?在Throwable及其子類中的構造器中都可以接受一個 cause 引數,該引數儲存了原有的異常資訊,通過 getCause() 就可以獲取該原始異常資訊。

自定義異常

使用者自定義異常類,只需繼承Exception類即可。在程式中使用自定義異常類,大體可分為以下幾個步驟。

(1)建立自定義異常類。

(2)在方法中通過 throw 關鍵字丟擲異常物件。

(3)如果在當前丟擲異常的方法中處理異常,可以使用try-catch語句捕獲並處理;否則在方法的宣告處通過 throws 關鍵字指明要丟擲給方法呼叫者的異常,繼續進行下一步操作。

(4)在出現異常方法的呼叫者中捕獲並處理異常。

一般情況下自定義檢查性異常比較少,常見是根據業務邏輯自定義一些執行時異常,並給這些異常分配errorcode 和自定義的異常封裝資訊。

實踐小結

  1. 不要在finally塊中處理返回值。如果try中有return語句,會將return的值存在其他地方,finally中對return的值的修改並未作用到try中的return值上。

  2. 不要在建構函式中丟擲異常。

  3. 不要將將異常直接顯示在頁面或客戶端,可以自定義異常類,將異常分配errorcode並可以自定義異常封裝資訊。

  4. 避免對程式碼層次結構的汙染,比如dao層的sql異常就在dao層來處理

  5. 異常處理佔用系統資源,不要將異常包含在迴圈語句塊中,有時候處理的不得當還有可能造成死迴圈(比如在某次迴圈中發生異常導致異常後面的程式碼無法執行以致於不能達到迴圈結束條件)

  6. 避免多層次封裝丟擲非檢查性異常。比如將所有的 Exception 再轉換成 RuntimeException 丟擲,如果 Exception 的型別已經是 RuntimeException 時,又做了一次封裝將丟失了原有的 RuntimeException 攜帶的有效資訊。解決辦法是判斷捕獲的異常是不是 RuntimeException 的例項。如果是,將拷貝相應的屬性到新建的例項上;或者用不同的 catch 語句塊捕捉 RuntimeException 和其它的 Exception。

  7. 多層次列印異常。其實列印日誌只需要在程式碼的最外層列印就可以了,異常列印也可以寫成 AOP,織入到框架的最外層,或者可以利用攔截器或者過濾器實現日誌的列印,降低程式碼維護、遷移的成本。可以將引數資訊新增到異常資訊中,方便問題定位。

  8. 發現程式中潛在的異常。在寫程式碼的過程中,由於對呼叫程式碼缺乏深層次的瞭解,不能準確判斷是否呼叫的程式碼會產生異常,因而忽略處理。在產生了 Production Bug 之後才想起來應該在某段程式碼處新增異常捕獲,甚至不能準確指出出現異常的原因。這就需要開發人員不僅知道自己在做什麼,而且要去儘可能的知道別人做了什麼,可能會導致什麼結果,從全域性去考慮整個應用程式的處理過程。這些思想會影響我們對程式碼的編寫和處理。

參考的第一篇文章中的題目也挺有意思的。


[參考資料]

深入理解java異常處理機制
Java 異常處理的誤區和經驗總結

相關文章