程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> 計算機程序的思維邏輯 (25),思維邏輯

計算機程序的思維邏輯 (25),思維邏輯

編輯:JAVA綜合教程

計算機程序的思維邏輯 (25),思維邏輯


上節我們介紹了異常的基本概念和異常類,本節我們進一步介紹對異常的處理,我們先來看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編程及計算機技術的本質。用心寫作,原創文章,保留所有版權。

-----------

更多好評原創文章

計算機程序的思維邏輯 (1) - 數據和變量

計算機程序的思維邏輯 (5) - 小數計算為什麼會出錯?

計算機程序的思維邏輯 (6) - 如何從亂碼中恢復 (上)?

計算機程序的思維邏輯 (8) - char的真正含義

計算機程序的思維邏輯 (12) - 函數調用的基本原理

計算機程序的思維邏輯 (17) - 繼承實現的基本原理

計算機程序的思維邏輯 (18) - 為什麼說繼承是把雙刃劍

計算機程序的思維邏輯 (19) - 接口的本質

計算機程序的思維邏輯 (20) - 為什麼要有抽象類?

計算機程序的思維邏輯 (21) - 內部類的本質

計算機程序的思維邏輯 (23) - 枚舉的本質

計算機程序的思維邏輯 (24) - 異常 (上)

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved