每個軟件都可能遇到異常,所以從設計階段就要考慮異常處理的問題,納為業務流程的一部分。
異常是需要妥善處理的,但是處理的前提是發現異常,而發現異常的前提的對異常有清楚的認識,我們要先認識到程序中都有什么樣的異常(定義異常),然后在程序結構中檢測和拋出異常(捕獲異常),最后用恰當的業務流程去分別處理(處理異常)
所以,發現和處理異常的過程可以簡單歸納為定義->發現->處理的過程,也就是定義異常-->捕捉異常->處理異常。
一、定義異常
要定義異常,就要看看程序都可能有哪些異常,如何去為這些異常分類。
異常當前不只一種,只有一種異常是不能滿足業務流程需要的。
例如在線登錄失敗這種異常,只拋出一個“登錄失敗”是無法準確處理的,失敗是因為網絡連接失敗?還是用戶名密碼錯誤?這兩種錯誤類型,要分別去走不同的業務流程,一個要檢查網絡,一個要檢查用戶名密碼,只有定義成不同的異常,才能根據實際情況去引導用戶分別操作。
1.Throwable
在Java中,異常的基類是Throwable,它只引用了Serializable這個可序列化接口,基于Throwable,還有Exception、RuntimeException和Error這三個類,這幾個類之間的關系如下:
我們看到從大的分支上來看,分為Error和Exception兩大類,我們先看看這兩類有什么區別。
2.Error和Exception
我們看一組對比:
Error ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? Exception
check ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?uncheck(運行時)
主要在編譯時提示 ? ? ? ? ? ? ? 運行時提示
不建議捕獲 ? ? ? ? ? ? ? ? ? ? ? ? ?建議捕獲
Error錯誤,主要在開發時起檢查作用(check),編譯器在編譯時,根據已知可能存在的異常,提示開發者,例如,把一個String值直接賦給int對象,編譯器就會提示Error了。
Exception異常,是編譯器檢查不出來的(uncheck),是在軟件運行時才會發現的異常,也就是運行時異常(RuntimeException),例如,做一個3/0的運算,這是個算術錯誤,但是編譯器在編譯階段看不出錯誤,只有在運行階段才能發現異常。
Error和Exception這兩種異常,其實都是可以用try catch捕獲的,寫catch(Error e)/catch(Exception e)就可以捕獲,但是一般不建議捕獲Error錯誤,這是為什么呢?
因為分工不同,先看Exception,這是運行階段可能出現的異常,一般在某些特殊邏輯分支或參數下,才可能出現,開發者也應該對這種邏輯進行處理,所以建議捕獲異常進行處理;
但是Error是不建議捕獲的,前面說了,Error不是運行階段可能出現的錯誤,他本身就代表程序邏輯有硬傷,或者運行環境不正確,在這種情況下,即便是捕獲了異常,程序也沒有辦法繼續執行,所以建議不捕獲。
我們找兩個例子,ClassNotFoundException和NoClassDefFoundError,這兩個看起來都是找不到類導致的異常,但是一個是Exceptioin異常,一個是Error錯誤,我們對比一下,就能理解Error和Exception的區別了。
ClassNotFoundException,是個Exception異常,一般在反射時遇到,是動態加載時報錯的,動態加載是開發者故意設計的業務邏輯,本身就有失敗的可能,所有建議捕獲異常。
NoClassDefFoundError,是個Error錯誤,這個錯誤發生時,在編譯時都沒有問題,但是運行時,JVM或者ClassLoader去加載某個類,發現這個類找不到了,就會報這個錯誤。這一般是運行環境的問題,例如缺少庫文件什么的,這個錯誤與業務邏輯無關,是必須解決掉的錯誤,否則軟件無法繼續運行,所以不建議捕獲異常。
3.異常子類
異常子類一般都是Exception的子類,Java提供了豐富的異常類,開發者也可以自定義異常類,異常類的作用就是描述什么出了錯,和為什么出錯,例如:IllegalArgumentException("filepath is null"),就拋出了一個參數錯誤的異常,而且說明了出錯的原因是"filepath is null"。
在實際開發中,我們需要根據自己的業務場景,去選用或自定義異常類型,根據實際情況去拋出異常。
二、捕獲異常
前面一直在說定義異常的問題,接下來我們要捕獲到這些異常。在出現異常時,我們需要立即知道哪里出了異常,為什么會出異常,具體來說,就是定位到異常代碼,并為異常分類,去定義這個異常的消息內容。
1.定位
Java可以比較容易地定位到異常代碼,異常堆棧提供了導致異常的方法調用鏈,能精確定位到類名,方法,代碼行。
2.分類
分類就需要一定的設計經驗了,一方面要提前做好異常定義,另一方面要在代碼中準確拋出異常。
例如:在讀取文件時,把文件地址作為參數,如果輸入一個空的文件地址,Java默認只會報一個NullPointerException空指針異常,也沒有異常消息,這種寬泛的分類下,我們只知道這里出現了異常,卻不知道為什么會異常,后面的異常處理就沒辦法做。
這種情況下,我們為了能精準地處理空文件地址的問題,就需要自己去判斷文件名是否為空,如果為空,則拋出一個IllegalArgumentException,并自定義一個"filepath is null"的異常消息傳出來,這樣后面處理時,就可以在這種情況下提示用戶“請輸入文件地址”,而不是簡單粗暴地報一句“出錯啦”了事。
3.捕獲
異常捕獲是需要融入到代碼邏輯中的,首先要預見可能的異常,然后定義異常及其異常消息,最后才能在代碼段中捕獲到相應的異常。
4.拋出
有時候,我們需要主動拋出異常,比如我們不希望用戶調用某些函數,或在某些邏輯分支中提前判斷并拋出異常,我們可以主動在代碼里throw一些異常,比如throw methodErr("visiting this method is not allowed");
三、處理異常
我們捕獲異常,最終都是為了走恰當的業務流程,去處理異常。
Java有兩種處理異常的方式,一是自己捕獲,用try catch去捕捉異常,在catch代碼里處理;另一種是讓調用者捕獲,用throw拋出異常,通知調用者去處理。
這兩種處理方式不是隨便選擇的,要看具體的業務,異常應該馬上捕獲,但不一定要馬上處理。
例如:服務器查詢數據庫時,業務層查詢數據,會調用數據訪問層,這時如果數據庫連接失敗就會出現異常,這個異常如果在數據層自己捕獲到,就僅限于數據層知道了,業務層根本不知道出了異常,會誤以為數據庫中沒有這種數據。這時正確的做法應該是拋出異常,用throw向業務層拋出異常,讓業務層自己去處理,決定是重試連接,還是告知用戶。
異常的提示,是與業務相關的,如果需要用戶走不同的邏輯分支,就需要設計相關的界面和提示;如果需要反饋給開發者,就需要記錄日志并上傳到服務器。