電腦程式的思維邏輯 (25) - 異常 (下)

来源:http://www.cnblogs.com/swiftma/archive/2016/07/11/5658132.html
-Advertisement-
Play Games

本節進一步介紹對異常的處理,finally的詭異之處,checked/unchecked exception的區別,異常的來源,以及處理的思維邏輯 ... ...


上節我們介紹了異常的基本概念和異常類,本節我們進一步介紹對異常的處理,我們先來看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) - 異常 (上)

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 有以下文本 要求匹配 註釋外的內容,匹配之後的內容: aaa bbb ddd eee hhh 可以利用/^xxx/../^xxx/結構來匹配 ,但是出現了以下的嵌套結構: aaa bbb #if defined(lxx_mmi_del) ccc #endif ddd eee #if defined( ...
  • 一、文件的編碼 分析: * 1. “& 0xff”的解釋: * 0xFF表示的是16進位(十進位是255),表示為二進位就是“11111111”。 * 那麼&符表示的是按位數進行與(同為1的時候返回1,否則返回0) * 2.位元組byte與int類型轉換: * Integer.toHexString( ...
  • 原文鏈接: "克裡斯的小屋——Python 實現的關鍵詞查找小工具" 引言 平時工作時,有時會遇到這樣的情景:在一個目錄及其子目錄下所有的文本文件中查找某個關鍵字、詞或者完整的句子。當然,如果是在 平臺上, 就能實現這樣的功能。不過最近學習了 相關的知識,自然是想做出一款帶有界面的小工具,可以跨平臺 ...
  • 互聯網的發展史上,安全性一直是開發者們相當重視的一個主題,為了實現數據傳輸安全,我們需要保證:數據來源(非偽造請求)、數據完整性(沒有被人修改過)、數據私密性(密文,無法直接讀取)等。 ...
  • 本文根據官方文檔全面講解了shinydashboard包的安裝與使用,希望有幫助! ...
  • 根據日誌,確實發生了FullGC,計算資源被耗光 ...
  • 1、拆分消息隊列 原本在登錄註冊的時候需要使用到簡訊發送,這個需要使用到消息隊列,當時只放入在項目中 現在的新需求在各個環節都有不同的消息推送,短息服務,以及日誌保存,這些索性單獨拎出來作為一個服務提供 (消息隊列採用RabbitMQ,各位看管有興趣可以參考之前發的文章,另外MQ也有ActiveMQ ...
  • 一、MVC概要 MVC是模型(Model)、視圖(View)、控制器(Controller)的簡寫,是一種軟體設計規範,用一種將業務邏輯、數據、顯示分離的方法組織代碼,MVC主要作用是降低了視圖與業務邏輯間的雙向偶合。MVC不是一種設計模式,MVC是一種架構模式。當然不同的MVC存在差異。 在web ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...