1. 程式人生 > >Java程式設計思想 第十二章:通過異常處理錯誤

Java程式設計思想 第十二章:通過異常處理錯誤

發現錯誤的理想時機是在編譯階段,也就是程式在編碼過程中發現錯誤,然而一些業務邏輯錯誤,編譯器並不能一定會找到錯誤,餘下的問題需要在程式執行期間解決,這就需要發生錯誤的地方能夠準確的將錯誤資訊傳遞給某個接收者,以便接收者知道如何正確的處理這個錯誤資訊。

改進錯誤的機制在Java中尤為重要,Java使用異常來提供一致性的錯誤報告,使得程式構件可以與客戶端程式碼可靠地溝通問題所在。本文學習如何編寫正確的異常處理程式,並將展示方法出現問題時,如何自定義異常。

1.概念

C語言以及其它早期語言的錯誤處理方式通常是一些約定俗成的模式,比如返回某個錯誤標記或者設定某個特殊的值通過判斷值來確認是否發生了錯誤。然而長期來看這種錯誤處理方式由於需要大量的判斷以及細緻的錯誤檢查而使程式碼邏輯較為複雜,因此不利於構建大型健壯性的系統。

解決的方法是,用強制規定的形式來消除錯誤處理過程中隨心所欲的因素。“異常”這個詞有“我對此感到意外”的意思,當錯誤問題出現了,你可能不知道出現在哪裡,或者出現了什麼錯誤,也不知道怎麼解決。那麼就停下來,將這個問題提供給更高的環境,看看有沒有正確的解決方案。使用異常的另一個好處是,它能夠明顯的降低程式碼的複雜程度,避免了大量的錯誤檢查,只需要在一個特定的地方進行異常捕獲,並且不需要做任何判斷,異常捕獲區能夠捕獲所有發生的錯誤。這種異常的處理方式與之前的錯誤處理方式相比,完全的將“正常做的事兒”與“出現問題怎麼辦”隔離開來。是程式碼的讀寫變得更加井井有條

2. 基本異常

“異常情形”是指阻止當前方法或者作用域繼續執行的問題,要把異常情形與普通問題區分開來,普通問題是指在當前的錯誤收集情況下,能夠解決這個問題並繼續執行正常的程式。而對於異常情形,這個程式就不能夠正常的執行下去了。

除法是一個簡單的例子,除數有可能為0,所以先進行檢查很有必要。但除數為0如果是一個意外的值,你也不清楚該如何處理,那麼丟擲異常就顯得尤為重要了,而不是順著原來的路走下去。

丟擲異常的核心本質:

  1. 同 Java中其它物件的建立相同,將使用new在堆上建立物件。
  2. 當前程式的執行路徑被終止了,因為發生了異常,不能繼續執行下去,並且從當前執行的環境中彈出異常物件的引用。
  3. 此時,異常處理機制接管程式,並開始尋找一個恰當的地方繼續執行程式,這個恰當的地方就是異常處理程式。它的任務是使程式從錯誤的狀態中恢復,要麼換一種方式執行,要麼繼續執行下去。

舉一個丟擲異常的簡單例子,對於一個物件引用t,傳遞給你的時候可能沒有被初始化,所以在使用這個物件引用呼叫執行方法之前,進行合理的判斷是非常有必要的。可以建立一個代表錯誤資訊的物件,並且將它從當前環境中丟擲,這樣就把錯誤異常拋到更大的環境中去了。所以一個異常,看起來是這樣的:

if(t==null){
    throw new NullPointerException();
}

異常情形是指阻止當前方法或作用域繼續執行的問題。

異常處理程式將程式從錯誤狀態中恢復,以使程式要麼換一種方式執行,要麼繼續執行下去。

在沒有其它辦法的情況下,異常允許我們強制程式停止執行,並告訴我們出現了什麼問題。理想狀態下,還可以強制程式處理問題,並返回到穩定狀態的。

3.捕獲異常

監控區域是一個可能產生異常的程式碼,並且後面跟著處理這些異常的程式碼。

如果在方法內部丟擲了異常,那麼這個方法就此結束。如果不希望這個方法結束,那麼可以在方法內設定一個特殊的塊來捕獲異常,即try塊。為什麼叫try呢,因為在這個塊裡“嘗試”各種可能產生異常的方法進行呼叫,所以是try。

try {
// Code that might generate exceptions
} catch(Type1 id1)|{
// Handle exceptions of Type1
} catch(Type2 id2) {
// Handle exceptions of Type2
} catch(Type3 id3) {
// Handle exceptions of Type3
}

異常丟擲後,異常處理機制將搜尋引數與異常型別相匹配的第一個處理程式,進入catch語句處理,此時認為異常的到了處理。catch子句結束,則處理程式不再往下找匹配了。

異常處理理論上有兩種基本模型:

  1. java支援終止模型。該模型假設錯誤非常關鍵,一旦異常被丟擲,那麼錯誤已經無可挽回,程式不能繼續執行。
  2. 恢復模型,就是先修正錯誤,然後重新進入該方法。這個模型假定了修正完之後再進入執行一定會成功。

4.建立自定義異常

可以異常類不寫建構函式,使用預設無參建構函式,也可以寫建構函式。醬紫可以實現在丟擲的異常後面打印出異常所在函式等功能。比如:

class MyException extends Exception { 
       public MyException() {} 
       public MyException(String msg) { super(msg); } 
     } 

在丟擲異常時

 public static void g() throws MyException { 
          System.out.println("Throwing MyException from g()"); 
          throw new MyException("Originated in g()"); 
       }

那麼,在列印的時候,就可以打印出

MyException: Originated in g() 

5.異常說明

Java鼓勵人們把方法可能會丟擲的異常告知使用此方法的客戶端程式設計師。異常說明使用了附加的關鍵字throws,後面接一個所有異常在異常型別的列表,所以方法定義如下:

void f() throws TooBig, TooSmall, DivZero { //...

這種在編譯時被強制檢查的異常稱為被檢查的異常。

也可以宣告方法將丟擲異常,但是實際上卻不丟擲。這樣做可以先為異常佔個位置,以後可以丟擲這類異常而不用修改已有方法,這種“作弊”方法通常用在你定義抽象基類和介面時,這樣派生類或者介面實現就能丟擲這些預先宣告的異常。

6.捕獲所有異常

捕獲異常型別的基類Exception(還有其它基類),這可以保證異常一定會被捕獲,最好把它放到異常處理程式列表的末尾。

catch(Exception e) {
System.out.println("Caught an exception");
}

Exception可以呼叫其從基類繼承的方法:

  1. String getMessage( )
  2. String getLocalizedMessage( )

獲取詳細資訊(丟擲異常物件所帶的引數),或者用本地語言表示的詳細資訊。

列印Throwable和Throwable的呼叫棧軌跡:

  1. void printStackTrace( )
  2. void printStackTrace(PrintStream)
  3. void printStackTrace(java.io.PrintWriter)

6.1 棧軌跡

printStackTrace()方法所提供的資訊可以通過getStackTrace()方法來直接訪問,該方法返回一個由棧軌跡元素所構成的陣列,每個元素表示棧中的一幀,元素0也是棧頂元素,是最後呼叫的方法(Throwable被建立和丟擲之處),最後一個元素是棧底,是呼叫序列的第一個方法呼叫。

6.2 重新丟擲異常

擋在異常處理模組裡繼續丟擲異常,那麼printStackTrace()方法顯示的將是原來異常丟擲點的呼叫棧資訊,而非重新丟擲點的的資訊。

catch(Exception e) {
System.out.println("An exception was thrown");
throw e;
}

此時可以使用fillinStackTrace()方法

catch(Exception e) {
System.out.println("An exception was thrown");
throw (Exception)e.fillInStackTrace();
}

呼叫fillInStackTrace()的這一行就成為異常的新發生地了。在異常捕獲之後丟擲另一種異常,其效果類似於fillInStackTrace()。

7.Java標準異常

hTrowable物件可分為兩種型別(指從Throwable繼承而得到的型別):Error用來表示編譯時和系統錯誤,Exception是可以被丟擲的基本型別,包括Java類庫,使用者方法以及執行時故障都可以丟擲此異常。

Error一般不用自己關心,現在來講Exception:

7.1 特例RuntimeException

比如nullPointerException,空指標異常。執行時產生的異常,不需要在異常說明中宣告方法將丟擲RuntimeException型別的異常。它們被稱為“不受檢查的異常”。這種異常屬於錯誤,會被自動捕獲,而不用程式設計師自己寫程式碼捕獲。

如果RuntimeException沒有被捕獲而直達main(),那麼在程式退出前將呼叫異常的printStackTrace()方法。

8.使用finally進行清理

在異常處理程式後面加上finamlly子句,可保證無論try塊裡的異常是否丟擲,都能執行。(通常適用於記憶體回收之外的情況)

finally執行未必要放在最後,正常的順序執行到它就是它了。

try {
// The guarded region: Dangerous activities
// that might throw A, B, or C
} catch(A a1) {
// Handler for situation A
} catch(B b1) {
// Handler for situation B
} catch(C c1) {
// Handler for situation C
} finally {
// Activities that happen every time
}

9. 異常的限制

當覆蓋 方法時,只能丟擲在基類方法的異常說明裡列出的那些異常。這個限制意味著,當基類程式碼運用到派生類時,依舊有用。

當處理派生類物件時,編譯器只會強制要求捕獲派生類該方法產生的異常。如果向上轉型為基類,編譯器會要求捕獲基類方法產生的異常。很智慧的。
異常說明本身並不屬於方法型別的範疇中,因此不參與過載的判斷。

基於特定方法的“異常說明的介面”不是變大了而是變小了,小於等於基類異常說明表——這恰好和類介面在繼承時的情形相反。

10.構造器

如果在構造器中丟擲了異常,這些清理行為也就不會正常工作了。這就意味著在編寫構造器時要格外小心。有人可能認為使用finally就可以解決問題了。但問題並不是那麼簡單,因為finally每次都會執行清理程式碼。如果構造器在其執行過程中半途而廢,也許該物件的某些部分還沒有成功建立,而這些部分在finally子句中卻是要別清理的。

11. 異常的匹配