Java中異常處理的9個最佳實踐

2020-09-05 碼農神說

在Java中進行處理異常並非是一件容易的事,初學者經常陷入困惑,甚至有經驗的開發者也需要認真研討哪些異常需要處理,哪些異常需要向上拋出。導致每個開發團隊都會自己定製一套特有的異常處理規則,這使得新加入團隊的成員都經歷一段痛苦的適應期。

儘管如此,前輩們依然總結了幾個最佳實踐可以遵循,這些實踐被絕大多數的團隊所採用,本文將為你列出9個最常用且最重要的實踐來幫助你提升異常處理的技能。

在做任何事的行動之前,知道為什麼做?做了能解決什麼問題?然後才去思考怎麼做!這樣不僅會讓你思路更清晰,還可以讓這件事更有價值。因此在進入探討異常處理最佳實踐的正題之前,我們首先需要解決兩個問題:

  1. 什麼是異常和異常處理?
  2. 為什麼需要它們?

異常及異常處理

什麼是異常?總結為一句話就是:程序在執行過程中產生的異常情況。當某些事情出現了錯誤異常就會發生,比如打開一個並不存在的文件、嘗試在一個為null的對象上調用方法等等,都會發生異常。

異常是不可預知的,可是一旦它發生了就需要進行異常處理,可謂知錯就改善莫大焉!異常處理是一種錯誤處理機制,如果你不對異常做任何處理,異常將會導致應用程式崩潰。

一旦你選擇了進行處理異常,也就意味著你承認問題的發生,採用必要要的措施去讓應用程式從錯誤中恢復,從而讓業務繼續進行,阻止應用程式崩潰。

但比較古老的如C語言是通過返回錯誤碼的方式來處理異常的。比如數組越界比較常用的返回值是-1。

這種方式的優點是代碼邏輯易於推理,沒有中斷和代碼跳轉。另一方面,這種處理方式鼓勵函數的調用者總是檢查返回的錯誤碼。但是這種檢查容易造成代碼汙染,導致代碼的可讀性和可維護性降低。

錯誤代碼的另一個嚴重的缺點是缺乏上下文信息,你可能知道錯誤碼「-5」代表找不到文件,但究竟找不到哪個文件呢!錯誤碼就無法表述了。

錯誤代碼一般用於面向過程的語言,對面向對象的高級語言,有些場景是無能為力的,比如構造函數異常,是無法返回錯誤碼的。

異常處理

當異常被拋出時,應用程式的流程就會被中斷,如果沒能及時處理異常,應用程式將崩潰。用戶將看到異常信息,但那些信息大多他們是看不懂的,這將是一個很糟糕的用戶體驗,實際上異常信息還可以包裝成非常友好的提示。

所以必須進行異常處理,哪怕是為了提高用戶體驗、記錄問題日誌、優雅地退出應用程式等。

我們可以使用代碼塊(try...catch...)來進行異常處理,當發生異常,通過try執行代碼,如果發生異常,應用程式的流程將轉移到catch中,catch捕捉到異常並進行必要的處理。

以上表述的異常處理原理對初學者依然比較抽象,我們來舉個例子

分析下這個程序,在main中初始化有3個元素的數組,把這個數組傳遞給私有方法print4thItemInArray,在print4thItemInArray中試圖獲取數組的第4個元素,由於沒有第4個元素將拋出「ArrayIndexOutOfBoundsException」異常,應用程式只會列印到「Third line」。

執行應用輸出結果如下


First line

Second line

Third line

Exception in thread &34; java.lang.ArrayIndexOutOfBoundsException: 3

at com.zqf.App.print4thItemInArray(App.java:20)

at com.zqf.App.main(App.java:14)

Process finished with exit code 1

現在我們修改下,增加異常處理

現在運行看看輸出


First line

Second line

Third line

Have no four items!

Fourth line

Fith line

實際上這次的異常依然會發生,因為第4個元素的確不存在,所以在&34;輸出之前就拋出了異常,中斷執行流程,但流程跳轉到catch語句塊了,catch只列印了一條「Have no four items」,繼續向下執行。

Java異常體系

在Java中,所有的異常都有一個共同的祖先Throwable,它有2個子類:Exception(異常)和Error(錯誤),它們又各自有大量的子類。Exception(異常)和Error(錯誤)的共性和區別:兩者都可以被捕捉,但前者可以被應用程式本身處理,後者是嚴重的,是無法恢復處理的。


最佳實踐

1.用Finally或Try-With-Resource清理資源

我們經常在try語句塊使用資源,比如InputStream,使用完後需要關閉。經常犯的錯誤是在try語句塊中關閉資源。如

這種方式看似非常完美,不會有異常拋出,所有的語句在try中執行,關閉IputStream釋放資源。但試想一下:如果在「inputStream.close()」語句之前就拋出異常,會怎樣呢?正常的流程會被中斷並跳轉,導致InputStream根本沒關閉。

因此,應該把清理資源的代碼放在finally或try-with-resource語句中。不管是正常執行完try語句塊,還是異常處理完畢,都會執行finally語句塊,而你需要確保在finally關閉所有打開的資源。

在JDK7引入了try-with-resource的語法,簡單來說當一個資源對象(如InputSteam對象)實現了AutoCloseable接口,那麼就可以在try關鍵字後的括號裡創建實例,當try-catch語句塊執行完畢後,會自動關閉資源,代碼也會簡潔許多。如下

2.拋出具體異常

你拋出的異常越具體越好,不熟悉你代碼的同事或者幾個月之後的你,可能需要調用你這些方法並進行異常處理,所以儘可能多的提供信息,讓你的API更容易理解,比如能用NumberFormatException就不要用 IllegalArgumentException,絕對避免直接使用不具體的Exception類。

3.做好注釋/文檔

只要你在方法聲明異常,就需要做好Javadoc的注釋。這點和上一條最佳實踐有相同的目標:提供給調用者儘可能多的信息,便於避免異常或進行異常處理。所以請確保你在Javadoc中添加了&34;聲明,並且描述了造成異常的情況。

4. 異常攜帶可描述的信息

這條最佳實踐和前面兩條有點相似,但這條提供的信息不單是給方法調用者看的,而更多的是為了給記錄日誌或監控工具提供的,便於排查異常。實際上一般的異常類名就已經描述了問題的類型,你不必提供大量的附加信息,簡潔凝練即可。比如NumberFormatException,當java.lang.Long構造函數拋出異常時會提供一句簡短且清晰的文本來描述。

NumberFormatException 的名字就已經告訴你異常的種類,它攜帶的信息僅告訴你提供的字符串會導致異常,但如果異常名字不能表達異常種類,就需要提供更多的信息。上述的異常信息如下


17:17:26,386 ERROR TestExceptionHandling:52 - java.lang.NumberFormatException: For input string: &34;

如果你仔細看下JDK的源碼,就會清楚java.lang.Long在構造器中做了各種校驗,當某些校驗失敗會調用NumberFormatException.forInputString,而靜態方法forInputString會把java.lang.Long的構造參數格式化後再構造一個新的NumberFormatException實例並拋出


5. 線捕捉子類異常

很多IDE都會幫助你進行最佳實踐,如果你先捕捉父類異常再捕捉子類異常,它們會告訴你後面的代碼不可到達或者警告已經被捕捉,因為是按照catch在在代碼中順序執行的。

所以如果先捕捉IllegalArgumentException,將不能捕捉到其子類NumberFormatException,因此最佳時間是總是先捕捉更多信息的異常(子類),再捕捉父類。如


6. 不要捕捉Throwable

從Java異常體系的圖中可知,Throwable是所有異常(Exception)和錯誤(Error)的祖先,Throwable是可以被捕捉,但請不要捕捉。如果你捕捉了Throwable,那麼不僅僅是捕捉了異常,還捕捉了錯誤。但錯誤是無法恢復,它是被JVM拋出的嚴重錯誤,應用程式對這類錯誤是無能為力的。

7. 不要忽略異常

你是否記得曾幾何時,在分析bug時遇到代碼只執行了前半部分,但卻不知為何。有些開發者經常捕捉了異常,但憑經驗認為異常決定不可能發生,導致沒有做異常處理。

實際上在大多數情況下它都發生了,因為隨著時間和業務邏輯的變更,try代碼塊的內容變更了,導致了異常發生,而你的自信不僅害了你也害了後來人。建議catch中至少要留一條日誌,來告知異常問題,方便排查。

實際上在大多數情況下它都發生了,因為隨著時間和業務邏輯的變更,try代碼塊的內容變更了,導致了異常發生,而你的自信不僅害了你也害了後來人。建議catch中至少要留一條日誌,來告知異常問題,方便排查。

8. 不要在僅僅記錄日誌後向上拋出異常

「不要在僅僅記錄日誌後向上拋出異常」,這是最佳實踐中最容易被忽視的一條。你會發現在大量的代碼片段,甚至類庫中經常捕捉異常、記錄日誌,然後拋出異常。

直觀的感覺是記錄異常,然後拋出異常讓調用者可以恰當的處理,但同一個異常多處日誌記錄,會讓人迷惑,請參考第4條最佳實踐:簡潔凝練。

如果你真的需要給調用者提供更多信息,可以參考下一條最佳實踐:包裝異常。

9. 不消費包裝異常

比較可取的做法是捕捉到標準異常,根據實際業務自定義包裝異常再向上拋出。在包裝異常時通常把原始異常作為構造參數傳進來,否則會丟失棧的跟蹤信息,造成分析困難。

總結

如你所見,當你處理異常或拋出異常是有許多要考慮的事情,大多是從代碼的可讀性和API可用性來考慮。因此,最好和同事一起討論異常處理的最佳實踐,從而達成共識、步調一致,不僅提高工作效率,還能避免不可預知的異常。

相關閱讀


相關焦點

  • 處理Java異常的9個最佳實踐
    本文由程式設計師新視界原創翻譯英文原文連結:https://dzone.com/articles/9-best-practices-to-handle-exceptions-in-java說起Java異常處理,心情就變得沉重起來。
  • Java 處理 Exception 的 9 個最佳實踐
    不僅僅初學者很難理解,即使一些有經驗的開發者也需要花費很多時間來思考如何處理異常,包括需要處理哪些異常,怎樣處理等等。這也是絕大多數開發團隊都會制定一些規則來規範對異常的處理的原因。而團隊之間的這些規範往往是截然不同的。本文給出幾個被很多團隊使用的異常處理最佳實踐。1.
  • Java異常處理最佳實踐及陷阱防範
    無論我們做了多少準備,多少測試,這些異常總會在某個時間點出現,如果處理不當或是不及時,往往還會導致其他新的問題出現。所以我們要時刻注意這些陷阱以及需要一套「最佳實踐」來建立起一個完善的異常處理機制。在JDK中,Throwable是所有異常的父類,其下分為」Error「和」Exception「。Error意味著出現了不可控的嚴重錯誤,例如OutOfMemoryError。Exception則細分為兩類,受檢異常(check)需要我們手動try/catch或者在方法定義中throws,編譯器在編譯的時候會檢查其合法性。非受檢異常(uncheck)則不需要我們提前處理。
  • 別再瞎搞了,處理Java異常的10個最佳實踐
    如果有多個,那就分別拋出多個,這樣這個方法的使用者才會分別針對每個異常做特定的處理,從而避免發生故障。,並且預期是調用方能夠特殊的對他進行處理。在上面的例子中,如果someMethod()拋出一個異常,並且在finally塊中,cleanUp()也拋出一個異常
  • java中的try是什麼?|java的異常處理類型
    1、try-是java異常處理類型中的一個。那麼,異常處理又是啥?2、異常-說的就是編寫程序時出現的錯誤。2.1:java提供異常處理類Throwable2.2:Throwable類分為Error和Exception兩個子類2.3:Error由java本身來處理2.4:我們要捕獲和拋出的異常是Exception這類的異常
  • Java要避免的前5個異常處理編碼實踐
    通用異常永遠不應拋出Error,RuntimeException,Throwable和Exception異常處理程序應保留原始異常不應使用System.out或System.err(Throwable ex) { //Non-compliant code}Throwable是Java中所有錯誤和異常的超類。
  • java NullPointerException異常處理
    java語言與其他程式語言不同,它有嚴格的異常處理機制,如果需要拋出異常的代碼塊中沒有捕獲異常,程序將不會成功編譯,換言之:未使用異常捕獲、異常捕獲方式不正確等,都將導致代碼不能被jvm編譯。java標準庫,內建了大量的異常處理類,這些類以Throwable 設計為最頂層類。
  • Java學習(十二): 異常處理
    異常概述Java的異常處理機制也秉承著面向對象的基本思想。在Java中,所有的異常都是以類的類型存在。除了內置的異常類之外,Java也可以自定義異常類。此外,Java的異常處理機制也允許自定義拋出異常。
  • Java之異常處理
    Exception:其它因編程錯誤或偶然的外在因素導致的一般性問題,可以使用針對性的代碼進行處理。例如:空指針訪問試圖讀取不存在的文件網絡連接中斷數組角標越界異常的體系結構 * java.lang.Throwable * |-----java.lang.Error:一般不編寫針對性的代碼進行處理。
  • Java中最正確的異常處理方式
    比如說,你的代碼少了一個分號,那麼運行出來結果是提示是錯誤 java.lang.Error ;如果你用 System.out.println(11/0) ,那麼你是因為你用0做了除數,會拋出 java.lang.ArithmeticException 的異常。
  • Java提高篇——Java中的異常處理(絕對詳細,建議收藏)
    Java異常是一個描述在代碼段中發生異常的對象,當發生異常情況時,一個代表該異常的對象被創建並且在導致該異常的方法中被拋出,而該方法可以選擇自己處理異常或者傳遞該異常。MissingResourceException(丟失資源)、ClassNotFoundException(找不到類)等異常,這些異常是不檢查異常,程序中可以選擇捕獲處理,也可以不處理。
  • JAVA異常及其異常處理方式
    異常處理異常是程序中的一些錯誤,但並不是所有的錯誤都是異常,並且錯誤有時候是可以避免的。比如說,你的代碼少了一個分號,那麼運行出來結果是提示是錯誤 java.lang.Error;如果你用System.out.println(100/0),那麼你是因為你用0做了除數,會拋出 java.lang.ArithmeticException 的異常。
  • Java異常的十大問題總結
    未檢查的異常是由無法解決的問題引起的,例如被零除,空指針等。檢查的異常特別重要,因為您希望使用API的其他開發人員知道如何處理異常。2.異常管理的最佳實踐如果可以正確處理異常,則應將其捕獲,否則應將其引發。
  • java中的異常和日誌
    NO.5 java.lang.IllegalArgumentException  這個異常的解釋是 「方法的參數錯誤 「,很多J2ME的類庫中的方法在一些情況下都會引發這樣的錯誤,比如音量調節方法中的音量參數如果寫成負數就會出現這個異常,再比如g.setColor(int red,int green,int blue)這個方法中的三個值
  • Java程序優化的一些最佳實踐
    作者通過經歷的一個項目實例,介紹Java代碼優化的過程,總結了優化Java程序的一些最佳實踐,分析了進行優化的方法,並解釋了性能提升的原因。作者從多個角度分析導致性能低的原因,並逐個進行優化,最終使得程序的性能得到極大提升,增強了代碼的可讀性、可擴展性。
  • java安全編碼指南之:異常處理
    簡介異常是java程式設計師無法避免的一個話題,我們會有JVM自己的異常也有應用程式的異常,對於不同的異常,我們的處理原則是不是一樣的呢?異常簡介先上個圖,看一下常見的幾個異常類型。接下來我們來考慮一下java中線程的中斷異常。java中有三個非常相似的方法interrupt,interrupted和isInterrupted。
  • Java常見異常匯總!技術大牛的解決方案都在這兒了
    Java異常處理最佳實踐在 Java 中處理異常並不是一個簡單的事情。不僅僅初學者很難理解,即使一些有經驗的開發者也需要花費很多時間來思考如何處理異常,包括需要處理哪些異常,怎樣處理等等。這也是絕大多數開發團隊都會制定一些規則來規範進行異常處理的原因。
  • 帶你了解Java中的異常處理機制
    異常處理最根本的優勢就是將檢測錯誤(由被調用的方法完成)從處理錯誤(由調用方法完成)中分離出來。2. 異常類型異常:在Java語言中,將程序執行中發生的不正常情況稱為「異常」。一般是指編程時的邏輯錯誤,是程式設計師應該積極避免其出現的異常。java.lang.RuntimeException類及它的子類都是運行時異常。對於這類異常,可以不作處理,直接找到出現問題的代碼,進行規避。因為這類異常很普遍,若全處理可能會對程序的可讀性和運行效率產生影響。
  • Java基礎之異常處理機制
    ,如果處理不了異常,就將其交給調用者來處理;Java 異常體系:Java API文檔中的詳細介紹如下,來到catch代碼塊中執行我們定義的異常處理代碼;在上述案例中是列印出了異常信息,異常對象信息,異常棧追蹤;其中的3個方法都是Throwable類的方法:String getMessage():獲取異常的詳細描述信息;
  • 跟我學java編程—Java顯示異常信息與異常分類
    具體如下:● java.lang.ClassCastException:錯誤的類型轉換異常。● java.lang.ArrayIndexOutOfBoundsException:組下標越界異常。● java.lang.NullPointException:空指針訪問異常。