計算機程序的思惟邏輯 (25) - 異常 (下)

本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連接 html

上節咱們介紹了異常的基本概念和異常類,本節咱們進一步介紹對異常的處理,咱們先來看Java語言對異常處理的支持,而後探討在實際中到底應該如何處理異常。java

異常處理

catch匹配

上節簡單介紹了使用try/catch捕獲異常,其中catch只有一條,其實,catch還能夠有多條,每條對應一個異常類型,好比說:程序員

try{
    //可能觸發異常的代碼
}catch(NumberFormatException e){
    System.out.println("not valid number");
}catch(RuntimeException e){
    System.out.println("runtime exception "+e.getMessage());
}catch(Exception e){
    e.printStackTrace();
}
複製代碼

異常處理機制將根據拋出的異常類型找第一個匹配的catch塊,找到後,執行catch塊內的代碼,其餘catch塊就不執行了,若是沒有找到,會繼續到上層方法中查找。須要注意的是,拋出的異常類型是catch中聲明異常的子類也算匹配,因此須要將最具體的子類放在前面,若是基類Exception放在前面,則其餘更具體的catch代碼將得不到執行。數據庫

示例也演示了對異常信息的利用,e.getMessage()獲取異常消息,e.printStackTrace()打印異常棧到標準錯誤輸出流。經過這些信息有助於理解爲何會出異常,這是解決編程錯誤的經常使用方法。示例是直接將信息輸出到標準流上,實際系統中更經常使用的作法是輸出到專門的日誌中。編程

從新throw

在catch塊內處理完後,能夠從新拋出異常,異常能夠是原來的,也能夠是新建的,以下所示:數組

try{
    //可能觸發異常的代碼
}catch(NumberFormatException e){
    System.out.println("not valid number");
    throw new AppException("輸入格式不正確", e);
}catch(Exception e){
    e.printStackTrace();
    throw e;
}
複製代碼

對於Exception,在打印出異常棧後,就經過throw e從新拋出了。服務器

而對於NumberFormatException,咱們從新拋出了一個AppException,當前Exception做爲cause傳遞給了AppException,這樣就造成了一個異常鏈,捕獲到AppException的代碼能夠經過getCause()獲得NumberFormatException。微信

爲何要從新拋出呢?由於當前代碼不可以徹底處理該異常,須要調用者進一步處理。網絡

爲何要拋出一個新的異常呢?固然是當前異常不太合適,不合適多是信息不夠,須要補充一些新信息,還多是過於細節,不便於調用者理解和使用,若是調用者對細節感興趣,還能夠繼續經過getCause()獲取到原始異常。運維

finally

異常機制中還有一個重要的部分,就是finally, catch後面能夠跟finally語句,語法以下所示:

try{
    //可能拋出異常
}catch(Exception e){
    //捕獲異常
}finally{
    //無論有無異常都執行
}
複製代碼

finally內的代碼無論有無異常發生,都會執行。具體來講:

  • 若是沒有異常發生,在try內的代碼執行結束後執行。
  • 若是有異常發生且被catch捕獲,在catch內的代碼執行結束後執行
  • 若是有異常發生但沒被捕獲,則在異常被拋給上層以前執行。

因爲finally的這個特色,它通常用於釋放資源,如數據庫鏈接、文件流等。

try/catch/finally語法中,catch不是必需的,也就是能夠只有try/finally,表示不捕獲異常,異常自動向上傳遞,但finally中的代碼在異常發生後也執行。

finally語句有一個執行細節,若是在try或者catch語句內有return語句,則return語句在finally語句執行結束後才執行,但finally並不能改變返回值,咱們來看下代碼:

public static int test(){
    int ret = 0;
    try{
        return ret;
    }finally{
        ret = 2;
    }
}
複製代碼

這個函數的返回值是0,而不是2,實際執行過程是,在執行到try內的return ret;語句前,會先將返回值ret保存在一個臨時變量中,而後才執行finally語句,最後try再返回那個臨時變量,finally中對ret的修改不會被返回。

若是在finally中也有return語句呢?try和catch內的return會丟失,實際會返回finally中的返回值。finally中有return不只會覆蓋try和catch內的返回值,還會掩蓋try和catch內的異常,就像異常沒有發生同樣,好比說:

public static int test(){
    int ret = 0;
    try{
        int a = 5/0;
        return ret;
    }finally{
        return 2;
    }
}
複製代碼

以上代碼中,5/0會觸發ArithmeticException,可是finally中有return語句,這個方法就會返回2,而再也不向上傳遞異常了。

finally中不只return語句會掩蓋異常,若是finally中拋出了異常,則原異常就會被掩蓋,看下面代碼:

public static void test(){
    try{
        int a = 5/0;
    }finally{
        throw new RuntimeException("hello");
    }
}
複製代碼

finally中拋出了RuntimeException,則原異常ArithmeticException就丟失了。

因此,通常而言,爲避免混淆,應該避免在finally中使用return語句或者拋出異常,若是調用的其餘代碼可能拋出異常,則應該捕獲異常並進行處理。

throws

異常機制中,還有一個和throw很像的關鍵字throws,用於聲明一個方法可能拋出的異常,語法以下所示:

public void test() throws AppException, SQLException, NumberFormatException {
    //....
}
複製代碼

throws跟在方法的括號後面,能夠聲明多個異常,以逗號分隔。這個聲明的含義是說,我這個方法內可能拋出這些異常,我沒有進行處理,至少沒有處理完,調用者必須進行處理。這個聲明沒有說明,具體什麼狀況會拋出什麼異常,做爲一個良好的實踐,應該將這些信息用註釋的方式進行說明,這樣調用者才能更好的處理異常。

對於RuntimeException(unchecked exception),是不要求使用throws進行聲明的,但對於checked exception,則必須進行聲明,換句話說,若是沒有聲明,則不能拋出。

對於checked exception,不能夠拋出而不聲明,但能夠聲明拋出但實際不拋出,不拋出聲明它幹嗎?主要用於在父類方法中聲明,父類方法內可能沒有拋出,但子類重寫方法後可能就拋出了,子類不能拋出父類方法中沒有聲明的checked exception,因此就將全部可能拋出的異常都寫到父類上了。

若是一個方法內調用了另外一個聲明拋出checked exception的方法,則必須處理這些checked exception,不過,處理的方式既能夠是catch,也能夠是繼續使用throws,以下代碼所示:

public void tester() throws AppException {
    try {
        test();
    }  catch (SQLException e) {
        e.printStackTrace();
    }
} 
複製代碼

對於test拋出的SQLException,這裏使用了catch,而對於AppException,則將其添加到了本身方法的throws語句中,表示當前方法也處理不了,仍是由上層處理吧。

Checked對比Unchecked Exception

以上,能夠看出RuntimeException(unchecked exception)和checked exception的區別,checked exception必須出如今throws語句中,調用者必須處理,Java編譯器會強制這一點,而RuntimeException則沒有這個要求。

爲何要有這個區分呢?咱們本身定義異常的時候應該使用checked仍是unchecked exception啊?對於這個問題,業界有各類各樣的觀點和爭論,沒有特別一致的結論。

一種廣泛的說法是,RuntimeException(unchecked)表示編程的邏輯錯誤,編程時應該檢查以免這些錯誤,好比說像空指針異常,若是真的出現了這些異常,程序退出也是正常的,程序員應該檢查程序代碼的bug而不是想辦法處理這種異常。Checked exception表示程序自己沒問題,但因爲I/O、網絡、數據庫等其餘不可預測的錯誤致使的異常,調用者應該進行適當處理。

但其實編程錯誤也是應該進行處理的,尤爲是,Java被普遍應用於服務器程序中,不能由於一個邏輯錯誤就使程序退出。因此,目前一種更被認同的觀點是,Java中的這個區分是沒有太大意義的,能夠統一使用RuntimeException即unchcked exception來代替。

這個觀點的基本理由是,不管是checked仍是unchecked異常,不管是否出如今throws聲明中,咱們都應該在合適的地方以適當的方式進行處理,而不是隻爲了知足編譯器的要求,盲目處理異常,既然都要進行處理異常,checked exception的強制聲明和處理就顯得囉嗦,尤爲是在調用層次比較深的狀況下。

其實觀點自己並不過重要,更重要的是一致性,一個項目中,應該對如何使用異常達成一致,按照約定使用便可。Java中已有的異常和類庫也已經在哪裏,咱們仍是要按照他們的要求進行使用。

如何使用異常

針對異常,咱們介紹了try/catch/finally, catch匹配、從新拋出、throws、checked/unchecked exception,那到底該如何使用異常呢?

異常應該且僅用於異常狀況

這個含義是說,異常不能代替正常的條件判斷。好比說,循環處理數組元素的時候,你應該先檢查索引是否有效再進行處理,而不是等着拋出索引異常再結束循環。對於一個引用變量,若是正常狀況下它的值也可能爲null,那就應該先檢查是否是null,不爲null的狀況下再進行調用。

另外一方面,真正出現異常的時候,應該拋出異常,而不是返回特殊值,好比說,咱們看String的substring方法,它返回一個子字符串,它的代碼以下:

public String substring(int beginIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    int subLen = value.length - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
複製代碼

代碼會檢查beginIndex的有效性,若是無效,會拋出StringIndexOutOfBoundsException。純技術上一種可能的替代方法是不拋異常而返回特殊值null,但beginIndex無效是異常狀況,異常不能僞裝當正常處理

異常處理的目標

異常大概能夠分爲三個來源:用戶、程序員、第三方。用戶是指用戶的輸入有問題,程序員是指編程錯誤,第三方泛指其餘狀況如I/O錯誤、網絡、數據庫、第三方服務等。每種異常都應該進行適當的處理。

處理的目標能夠分爲報告和恢復。恢復是指經過程序自動解決問題。報告的最終對象多是用戶,即程序使用者,也多是系統運維人員或程序員。報告的目的也是爲了恢復,但這個恢復常常須要人的參與。

對用戶,若是用戶輸入不對,可能提示用戶具體哪裏輸入不對,若是是編程錯誤,可能提示用戶系統錯誤、建議聯繫客服,若是是第三方鏈接問題,可能提示用戶稍後重試。

對系統運維人員或程序員,他們通常不關心用戶輸入錯誤,而關注編程錯誤或第三方錯誤,對於這些錯誤,須要報告儘可能完整的細節,包括異常鏈、異常棧等,以便儘快定位和解決問題。

對於用戶輸入或編程錯誤,通常都是難以經過程序自動解決的,第三方錯誤則可能能夠,甚至不少時候,程序都不該該假定第三方是可靠的,應該有容錯機制。好比說,某個第三方服務鏈接不上(好比發短信),可能的容錯機制是,換另外一個提供一樣功能的第三方試試,還多是,間隔一段時間進行重試,在屢次失敗以後再報告錯誤。

異常處理的通常邏輯

若是本身知道怎麼處理異常,就進行處理,若是能夠經過程序自動解決,就自動解決,若是異常能夠被本身解決,就不須要再向上報告。

若是本身不能徹底解決,就應該向上報告。若是本身有額外信息能夠提供,有助於分析和解決問題,就應該提供,能夠以原異常爲cause從新拋出一個異常。

總有一層代碼須要爲異常負責,多是知道如何處理該異常的代碼,多是面對用戶的代碼,也多是主程序。若是異常不能自動解決,對於用戶,應該根據異常信息提供用戶能理解和對用戶有幫助的信息,對運維和程序員,則應該輸出詳細的異常鏈和異常棧到日誌。

這個邏輯與在公司中處理問題的邏輯是相似的,每一個級別都有本身應該解決的問題,本身能處理的本身處理,不能處理的就應該報告上級,把下級告訴他的,和他本身知道的,一併告訴上級,最終,公司老闆必需要爲全部問題負責。每一個級別既不該該掩蓋問題,也不該該逃避責任。

小結

上節和本節介紹了Java中的異常機制。在沒有異常機制的狀況下,惟一的退出機制是return,判斷是否異常的方法就是返回值。

方法根據是否異常返回不一樣的返回值,調用者根據不一樣返回值進行判斷,並進行相應處理。每一層方法都須要對調用的方法的每一個不一樣返回值進行檢查和處理,程序的正常邏輯和異常邏輯混雜在一塊兒,代碼每每難以閱讀理解和維護。

另外,由於異常畢竟是少數狀況,程序員常常偷懶,假定異常不會發生,而忽略對異常返回值的檢查,下降了程序的可靠性。

在有了異常機制後,程序的正常邏輯與異常邏輯能夠相分離,異常狀況能夠集中進行處理,異常還能夠自動向上傳遞,再也不須要每層方法都進行處理,異常也再也不可能被自動忽略,從而,處理異常狀況的代碼能夠大大減小,代碼的可讀性、可靠性、可維護性也均可以獲得提升。

至此,關於Java語言自己的主要概念咱們就介紹的差很少了,接下來的幾節中,咱們介紹Java中一些經常使用的類及其操做,從包裝類開始。


未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。用心原創,保留全部版權。

相關文章
相關標籤/搜索