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