上節我們介紹了異常的基本概念和異常類,本節我們進一步介紹對異常的處理,我們先來看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內的代碼不管有無異常發生,都會執行。具體來說:
由於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編程及計算機技術的本質。用心寫作,原創文章,保留所有版權。
-----------
更多好評原創文章
計算機程序的思維邏輯 (1) - 數據和變量
計算機程序的思維邏輯 (5) - 小數計算為什麼會出錯?
計算機程序的思維邏輯 (6) - 如何從亂碼中恢復 (上)?
計算機程序的思維邏輯 (8) - char的真正含義
計算機程序的思維邏輯 (12) - 函數調用的基本原理
計算機程序的思維邏輯 (17) - 繼承實現的基本原理
計算機程序的思維邏輯 (18) - 為什麼說繼承是把雙刃劍
計算機程序的思維邏輯 (19) - 接口的本質
計算機程序的思維邏輯 (20) - 為什麼要有抽象類?
計算機程序的思維邏輯 (21) - 內部類的本質
計算機程序的思維邏輯 (23) - 枚舉的本質
計算機程序的思維邏輯 (24) - 異常 (上)