本篇博客介紹Java中的異常機制及其基本使用。
什麼是異常?
首先,我們再來回顧一下剛開始接觸Java時犯的一些錯誤:
- 除0操作。
- 數組下標越界。
- 訪問null對象。
上述幾個出錯程序運行之後打出的Exception開頭的紅色的提示就是異常信息。那什麼是異常呢?
- 異常就是指程序運行時,出現錯誤時通知程序調用者的一種機制。
- 注意異常時運行時的一種機制,不是編譯期。我們可以舉個例子來理解一下什麼是編譯期的錯誤,比如我們寫代碼時出現關鍵字的拼寫錯誤,此時出現的錯誤就是編譯期的錯誤。
防禦式編程
什麼是防禦式編程?
- 防禦式編程時提高軟件質量技術的有益輔助手段;
- 防禦式編程的主要思想是:子程序應該不因傳入錯誤數據而被破壞,哪怕是由其他子程序產生的錯誤數據;
- 這種思想將可能出現的錯誤造成的影響控制在有限的範圍內。
錯誤在代碼中是客觀存在的。因此我們要讓程序出現問題的時候及時通知程序猿。主要有兩種方式:
- LBYL:Look Before You Leap,在操作之前就做充分的檢查;
- EAFP:It’s Easier to Ask Forgiveness than Permission,先操作,遇到問題再處理。
異常的核心思想就是EAFP(先操作,遇到問題再處理)。
異常的優點
我們通過一個簡單的例子來看一下網絡通信LBYL和EAFP的處理流程:
- LBLY方式。
- EAFP方式。
對比兩種風格的代碼,我們可以發現:
- LBLY處理方式會將正常流程和錯誤處理流程代碼混在一起,代碼整體顯得比較混亂;
- EAFP處理方式的正常流程和錯誤處理流程是分開的,更容易理解代碼。
異常的基本用法
異常捕獲
基本語法如下:
try {
// 可能出現異常的語句;
} [catch (異常類型 異常對象) {
// 異常的處理
} ...]
[finally {
// 異常出口
}]
- try代碼塊中放的是可能出現異常的代碼;
- catch代碼塊中放的是出現異常後的處理行爲;
- finally代碼塊中的代碼用於處理善後工作,會在最後執行;
- 其中catch和finally都可以根據情況選擇加或者不加。
下面我們看幾種常見的異常處理方式:
- 不處理異常。
可以看到,如果對於異常不進行處理,程序就會在出現異常處終止,後序的代碼將不再執行。其實這裏異常並不是沒有被處理,而是被JVM處理,JVM處理異常的方式就是打印出現異常的調用棧信息並終止程序。 - 使用try catch後的程序執行過程。
從運行結果可以看出,try中一旦有了異常,就會跳到對應的catch中,不再執行try中剩餘的邏輯。 - catch只能處理對應種類的異常。
可以看到,這裏的catch語句並沒有捕獲到數組訪問越界的異常,該異常最終被JVM處理。 - catch可以有多個。
一段代碼可能會拋出多種不同的異常,不同的異常有不同的處理方式。因此可以搭配多個catch代碼塊。如果多個異常的處理方式是完全相同的,也可以寫成如下形式:
- 也可以使用一個catch捕獲所有的異常。Exception類是所有異常類的父類。因此可以用這個類型表示捕獲所有異常。catch進行匹配的時候,不僅可以捕捉到相同類型的異常,還可以捕捉到目標類型異常的子類對象。不推薦使用這種方式。
- finally表示最後的善後工作,如釋放資源。
無論try中是否發生異常,finally中的代碼一定會執行。 - 可以使用try回收資源。
和前一個代碼的寫法等價,將Scanner對象在try的()中創建,能夠保證在try執行完畢後自動調用Scanner的close方法。 - 如果當前方法中沒有合適的異常處理方式,異常就會沿着調用棧向上傳遞,直到最後交給JVM處理。
異常處理流程
- 程序先執行try中的代碼;
- 如果try中的代碼出現異常,就會結束try中的代碼,看和catch中的異常類型是否匹配;
- 如果找到匹配的異常類型,就會執行catch中的代碼;
- 如果沒有找到匹配的異常類型,就會將異常向上傳遞到上層調用者;
- 無論是否找到匹配的異常類型,finally中的代碼都會被執行到(在該方法結束之前執行);
- 如果上層調用者也無法處理異常,異常就會繼續向上傳遞;
- 一直到main方法也沒有合適的代碼處理異常,就會交給JVM來進行處理,此時程序就會異常終止。
異常拋出
除了Java內置的類會拋出一些異常之外,程序猿也可以手動拋出某個異常。使用throw關鍵字來完成這個操作。
下面來看一個具體的例子:
異常說明
我們在處理異常時,通常希望知道這段代碼中究竟會出現哪些可能的異常。我們可以使用throws關鍵字,把可能拋出的異常顯式的標註在方法定義的位置。從而提醒調用者要注意捕獲。
finally的注意事項
我們知道finally中的代碼保證一定能夠被執行到,而且是在return語句之前,有時會帶來一些麻煩,我們看下面一個代碼:
注意:
- finally執行的時機是在方法返回之前(try或者catch中如果有return會在這個return之前執行finally);
- 但是如果finally中也存在return語句,那麼就會執行finally中的return,從而不會執行到try中原有的return;
- 不建議在finally中寫return語句,編譯器會有警告。
Java異常體系
- 頂層類Throwable派生出兩個重要的子類,Error和Exception;
- 其中Error指的是Java運行時內部錯誤和資源耗盡錯誤。應用程序不拋出此類異常。這種內部錯誤一旦出現,除了告知用戶並使程序終止之外,沒有別的辦法,這種情況很少出現;
- Exception是程序猿使用的異常類的父類;
- 其中Exception有一個子類稱爲RuntimeException,這裏面又派生出很多我們常見的異常類NullPointerException等。
Java語言規範將派生於Error類或RuntimeException類的所有異常稱爲非受查異常,所有的其他異常稱爲受查異常。
如果一段代碼可能拋出受查異常,那麼必須顯式進行處理。
從報錯信息可以看出,我們必須對受查異常進行處理。
這裏有兩種處理方式:
- 方法一:使用try catch包裹起來進來:
- 方法二:在方法上加上異常說明,相當於將處理動作交給上級調用者。
自定義異常類
Java中雖然已經內置了豐富的異常類,但是我們實際場景中可能還有一些情況需要我們對異常類進行擴展,創建符合我們實際情況的異常。
這裏,我們模擬一個用戶登錄的場景:
- 首先,我們自定義兩個異常類UserException和PasswordException。
- 下面我們來寫程序主邏輯。
- 運行效果如下:
注意:
- 自定義異常通常會繼承自Exception或者RuntimeException;
- 繼承自Exception的異常默認是受查異常;
- 繼承自RuntimeException的異常默認是非受查異常;
- 自定義異常類往往不是創建一個類,而是創建一個系列。