Java異常機制及異常處理建議


1、Java異常機制

       異常指不期而至的各種狀況,如:文件找不到、網絡連接失敗、非法參數等。異常是一個事件,它發生在程序運行期間,干擾了正常的指令流程。Java通過API中Throwable類的眾多子類描述各種不同的異常。因而,Java異常都是對象,是Throwable子類的實例,描述了出現在一段編碼中的錯誤條件。當條件生成時,錯誤將引發異常。

       Java異常類層次結構圖:

       在Java中,所有的異常都有一個共同的祖先Throwable(可拋出)類。Throwable指定代碼中可用異常傳播機制通過Java應用程序傳輸的任何問題的共性。

       Throwable有兩個重要的子類:Exception(異常)和Error(錯誤),二者都是Java異常處理的重要子類,各自都包含大量子類。

Java異常的分類

       Error(錯誤):是程序無法處理的錯誤,表示運行應用程序時出現的較嚴重的問題。大多數錯誤與代碼編寫者執行的操作無關,而表示代碼運行時JVM(Java虛擬機)出現的問題。例如,Java虛擬機運行錯誤(Virtual Machine Error),當JVM不再有繼續執行操作所需的內存資源時,將出現OutOfMemoryError。這些異常發生時,Java虛擬機(JVM)一般會選擇線程終止。這些錯誤表示故障發生於虛擬機自身、或者發生在虛擬機試圖執行應用時,如Java虛擬機運行錯誤(Virtual Machine Error)、類定義錯誤(NoClassDefFoundError)等。這些錯誤是不可查的,因為它們在應用程序的控制和處理能力之外,而且絕大多數是程序運行時不允許出現的狀況。對於設計合理的應用程序來說,即使確實發生了錯誤,本質上也不應該試圖去處理它所引起的異常狀況。在Java中,錯誤通過Error的子類描述。

       Exception(異常):是程序本身可以處理的異常。Exception類有一個重要的子類RuntimeException。RuntimeException類及其子類表示“JVM常用操作”引發的錯誤。例如,若在程序中試圖使用空值對象的引用、除數為零或數組訪問越界,則分別引發運行時異常——NullPointerException、ArithmeticException和ArrayIndexOutOfBoundException。

       注意:Exception和Error的區別,Exception能被程序本身可以處理,Error無法被程序處理

       通常,Java的異常(包括Exception和Error)分為可查的異常(checked exceptions)和不可查的異常(unchecked exceptions)。

       可查異常(checked exceptions):是編譯器要求必須處置的異常,正確的程序在運行過程中,很容易出現情理可容的異常狀況。可查異常雖然是異常狀況,但在一定程度上它的發生是可以預計的,而且一旦發生這種異常狀況,就必須采取某種方式進行處理。除了RuntimeException及其子類以外,其他的Exception類及其子類都屬於可查異常,最典型的是IO類異常。這種異常的特點是Java編譯器會檢查它,也就是說,當程序中出現了這類異常時,要么用try-catch語句捕獲它,要么用throws子句聲明拋出它,否則編譯不會通過。

       不可查異常(unchecked exceptions):編譯器不要求強制處置的異常,包括運行時異常,即RuntimeException與其子類和錯誤(Error)。排除Error的情況,還可以將所有的Exception分為兩大類:運行時異常和非運行時異常(非運行時異常也叫編譯異常)。程序中應當盡可能的去處理這些異常。

運行時Exception與非運行時Exception

       運行時異常:都是RuntimeException類及其子類異常,如空指針異常、下標越界異常等,這些異常是不可查異常,程序中可以選擇捕獲處理,也可以不處理。這些異常一般是由程序邏輯錯誤引起的,程序應該從邏輯角度盡可能避免這類異常的發生。

       運行時異常的特點是Java編譯器不會檢查它,也就是說,當程序中可能出現這類異常,即使沒有用try-catch語句捕獲它,也沒有用throws子句聲明拋出它,也能編譯通過。我們可以不處理運行時異常,當出現這樣的異常時,總是由虛擬機接管。比如:我們從來沒有人去處理過NullPointerException異常,它就是運行時異常,並且這種異常還是最常見的異常之一。出現運行時異常后,如果沒有捕獲並處理這個異常(即沒有catch),系統會把異常一直往上層拋,一直到最上層,如果是多線程就由Thread.run()拋出,如果是單線程就被main()拋出。拋出之后,如果是線程,這個線程也就退出了。如果是主程序拋出的異常,那么這整個程序也就退出了。也就是說,你如果不對運行時異常進行處理,那么出現運行時異常之后,要么是線程中止,要么是主程序終止。

       非運行時異常(編譯異常):是RuntimeException以外的異常,類型上也屬於Exception類及其子類。從程序語法角度講是必須進行處理的異常,如果不處理,程序就不能編譯通過。如IOException、SQLException等以及用戶自定義的Exception異常,對於這種異常,JAVA編譯器強制要求我們必需對出現的這些異常進行catch並處理,否則程序就不能編譯通過。所以,面對這種異常不管我們是否願意,只能自己去寫一大堆catch塊去處理可能的異常。

2、異常處理實踐

        Java中的異常處理不是一個簡單的話題。初學者很難理解,甚至有經驗的開發人員也會花幾個小時來討論應該如何拋出或處理這些異常。這就是為什么大多數開發團隊都有自己的異常處理的規則和方法。如果你是一個團隊的新手,你可能會驚訝於這些方法與你之前使用過的那些方法有多么不同。

在Finally中清理資源

       通常情況下,你在try中使用了一個資源,比如InputStream,之后需要關閉它。在這種情況下,一個常見的錯誤是在try的末尾關閉了資源。

public void doNotCloseResourceInTry() { FileInputStream inputStream = null; try { File file = new File("./tmp.txt"); inputStream = new FileInputStream(file); // use the inputStream to read a file // do NOT do this inputStream.close(); } catch (FileNotFoundException e) { log.error(e); } catch (IOException e) { log.error(e); } }

       問題是,只要不拋出異常,這種方法就可以很好地運行。try內的所有語句都將被執行,資源也會被關閉。但是你在try里調用了一個或多個可能拋出異常的方法,或者自己拋出異常。這意味着可能無法到達try的末尾。因此,將不會關閉這些資源。所以應該將清理資源的代碼放入Finally中,或者使用Try-With-Resource語句。

       相比於try,無論是在成功執行try里的代碼后,或是在catch中處理了一個異常后,Finally里的內容是一定會被執行的。因此,可以確保清理所有已打開的資源。

public void closeResourceInFinally() { FileInputStream inputStream = null; try { File file = new File("./tmp.txt"); inputStream = new FileInputStream(file); // use the inputStream to read a file } catch (FileNotFoundException e) { log.error(e); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { log.error(e); } } } }

不要捕獲繼承自RuntimeException的異常

       繼承自RuntimeException異常是運行時異常,如:IndexOutOfBoundsException、NullPointerException,這類異常由程序員預檢查來規避,保證程序健壯性。

//正例
if(obj != null) { ... } //反例: try { obj.method() } catch(NullPointerException e) { ... }

給出准確的異常處理信息

       你拋出的異常越具體越好。一定要記住,一個不太了解你代碼的同事,也許幾個月后,需要調用你的方法,並且處理這個異常。

       因此,請確保提供盡可能多的信息,這會使你的API更容易理解。因此,你方法的調用者將能夠更好地處理異常,或者通過額外的檢查來避免它。

       所以,要盡量能更好地描述你的異常處理信息,比如用NumberFormatException代替IllegalArgumentException,避免拋出一個不具體的異常

public void doNotDoThis() throws Exception {
    ... } public void doThis() throws NumberFormatException { ... }

記錄你所指定的異常

       當你在方法中指定一個異常時,你應該在Javadoc中記錄下它。這與前面提到的方法有着相同的目標:為調用者提供盡可能多的信息,這樣他們就可以避免異常或者更容易地處理異常。

       因此,請確保在Javadoc中添加一個@throws 聲明,並描述可能導致的異常情況。

/**
 * This method does something extremely useful ... * * @param input * @throws MyBusinessException if ... happens */ public void doSomething(String input) throws MyBusinessException { ... }

使用描述性消息拋出異常

       這一理念與前兩個相似。但這一次,你不用給調用方法的人提供信息。異常消息會被所有人讀取,同時必須了解在日志文件或監視工具中報告異常時發生了什么。

       如果拋出一個特定的異常,它的類名很可能已經描述了這種類型的錯誤。所以,你不需要提供很多額外的信息。一個很好的例子就是,當你以錯誤的格式使用字符串時,如NumberFormatException,它就會被類 java.lang.Long的構造函數拋出。

try { new Long("xyz"); } catch (NumberFormatException e) { log.error(e); }

       NumberFormatException已經告訴你問題的類型,所以只需要提供導致問題的輸入字符串。如果異常類的名稱不具有表達性,那么就需要提供必要的解釋信息

17:17:26,386 ERROR TestExceptionHandling:52 - java.lang.NumberFormatException: For input string: "xyz"

不要在catch中使用Throwable

        Throwable是exceptions 和 errors的父類。當然,你可以在catch子句中使用它,但其實你不應該這樣做。

        如果你在catch子句中使用Throwable,它將不僅捕獲所有的異常(Exception),還會捕獲所有錯誤(Error)。JVM會拋出錯誤,這是應用程序不打算處理的嚴重問題。典型的例子是OutOfMemoryError或StackOverflowError。這兩種情況都是由應用程序控制之外的情況引起的,無法處理

       所以,最好不要在catch中使用Throwable,除非你完全確定自己處於一個特殊的情況下,並且你需要處理一個錯誤。

public void doNotCatchThrowable() { try { // do something } catch (Throwable t) { // don't do this! } }

不要在catch中記錄異常然后又拋出這個異常

       你可以在許多代碼片段或者庫文件里發現,有異常會被捕獲、記錄和重新拋出。

try { new Long("xyz"); } catch (NumberFormatException e) { log.error(e); throw e; }

       當它發生時,記錄一個異常,然后重新拋出它,以便調用者能夠適當地處理它,這可能會很直觀。但是它會為同一個異常寫多個錯誤消息

17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz" Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.lang.Long.parseLong(Long.java:589) at java.lang.Long.(Long.java:965) at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63) at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58)

       如果你需要添加額外的信息,應該捕獲異常並將其包裝在一個自定義的信息中。有時最好捕獲一個標准異常並將其封裝到一個定制的異常中。此類異常的典型例子是應用程序或框架特定的業務異常。這允許你添加額外的信息,並且也可以為異常類實現一個特殊的處理。

public void wrapException(String input) throws MyBusinessException { try { // do something } catch (NumberFormatException e) { throw new MyBusinessException("A message that describes the error.", e); } }

將異常拋給函數調用者

       如果一個方法可能會出現異常,但其自身卻沒有能力處理這種異常,可以在方法聲明處用throws子句來聲明拋出異常,由上層的方法調用者處理。例如汽車在運行時可能會出現故障,汽車本身沒辦法處理這個故障,那就讓開車的人來處理。throws語句用在方法定義時聲明該方法要拋出的異常類型,如果拋出的是Exception異常類型,則該方法被聲明為拋出所有的異常。多個異常可使用逗號分隔。throws語句的語法格式為:

void fun() throws Exception1,Exception2,..,ExceptionN { } 

       方法名后的throws Exception1,Exception2,…,ExceptionN為聲明要拋出的異常列表。當方法拋出了異常列表中的異常時,方法將不對這些類型及其子類類型的異常作處理,而拋向上層調用該方法的方法,由它去處理。

       如果在函數體內用throw拋出了某種異常,最好要在函數名中加throws拋異常聲明,以便交給調用它的上層函數進行處理。捕獲異常是為了處理它,不要捕獲了卻什么都不處理而拋棄之,如果不想處理它,請將該異常拋給它 的調用者。最外層的業務使用者,必須處理異常,將其轉化為用戶可以理解的內容

只將非穩定代碼放在try-catch塊中

       一大段代碼進行try-catch,這是不負責任的表現。catch時請分清穩定代碼和非穩定代碼,穩定代碼指的是無論如何不會出錯的代碼。對於非穩定代碼的catch,要盡可能地進行區分異常類型,再做對應的異常處理。

不要在finally中寫return語句

       不能在 finally 塊中使用 return,finally塊中的return返回后方法結束執行,不會再執行try塊中的return語句。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM