1. 程式人生 > >【Java程式設計思想】12.通過異常處理錯誤

【Java程式設計思想】12.通過異常處理錯誤

Java 的基本理念是“結構不佳的程式碼不能執行”。
異常處理是 Java 中唯一正式的錯誤報告機制,並且通過編譯器強制執行。


12.1 概念

異常機制會保證能夠捕獲錯誤,並且只需在一個地方(即異常處理程式中)處理錯即可。


12.2 基本異常

異常情形(exceptional condition)是指組織當前方法或作用域繼續執行的問題。當前環境遭遇異常情形時,表示程式不能繼續下去,因為在當前環境下無法獲得必要的資訊來解決問題,能做的就是從當前環境下跳出,並且將問題提交給上一級環境。--這就是丟擲異常的場景了。
在丟擲異常之後,

  • 首先 Java 會使用 new 關鍵字在堆上建立一場物件
  • 然後當前的執行路徑會被終止,並且從當前環境中彈出對異常物件的引用。
  • 最後異常處理機制接管程式,並且開始尋找一個恰當的地方來繼續執行程式。

這個恰當的地方就是異常處理程式,它的任務是將程式從錯誤狀態中恢復,以使程式要麼換一種方式執行,要麼繼續執行下去。

標準異常類都有兩個構造器:一個是預設構造器,另一個接收字串作為引數,以便能把相關資訊放入異常物件的構造器。
在使用 new 建立了異常物件之後,此物件的引用將傳給 throw。可以用丟擲異常的方式從當前的作用域退出。這兩種情況下都將會返回一個一場物件,然後退出方法或作用域。
雖然看起來丟擲異常好像跟“方法返回”類似,但是,異常返回的“地點”和普通方法呼叫返回的“地點”完全不同。(異常會在一個恰當的異常處理程式中得到解決,位置離被丟擲的地方很遠,可能會跨越方法呼叫棧的很多層次。)

能丟擲任意型別的 Throwable 類是異常型別的根類。


12.3 捕獲異常

一般來說,方法內部或者方法內部呼叫丟擲異常時,這個方法會在丟擲異常的過程中結束。

try {
    // code that might generate exception
} catch {
    // handle exception
}

使用上述結構,可以在 try 內部程式碼丟擲異常的時候,捕獲到異常,並對異常進行處理。try 內的部分可以稱之為監控區域(guared region)。
每一個 catch 子句(異常處理程式)看起來就像一個接收且僅接收異常這種特殊型別引數的方法。當異常被丟擲時,異常處理機制會負責搜尋引數與異常型別相匹配的第一個處理程式,然後進入子句中執行,此時便認為異常得到了處理。一旦 catch 子句結束,則處理程式的查詢過程結束。

異常處理理論上有兩種基本型別。

  • 終止模型,這種模型中,會假設錯誤的出現會讓程式無法返回到異常發生的地方繼續執行。異常被丟擲意味著錯誤已經無法挽回。這也是 Java 和 C++ 所支援的模型。
  • 恢復模型,異常處理程式的工作室修正錯誤,然後嘗試重新調用出問題的方法。在 Java 中可以在 while 迴圈內放入 try 塊,達到類似的效果。這種效果一般會導致耦合度過高--恢復性處理程式的出口一般是非通用型程式碼(針對特殊異常情況),不好維護。

12.4 建立自定義異常

自己定義異常類,必須從已有的異常類繼承。
建立新的異常型別後,編譯器建立預設構造器,他將自動呼叫基類的預設構造器。
也可以定義一個接受字串引數的構造器作為錯誤資訊輸出。

對於呼叫了在 Throwable 類宣告的 printStackTrace() 的方法,將列印“從方法呼叫處直到異常丟擲處”的方法呼叫序列,資訊被髮送到 System.out,並自動地被捕獲和顯示在輸出中。
如果呼叫預設版本 e.printStackTrace(),則資訊將被輸出到標準錯誤流。

Logger 寫入的最簡單方式就是直接呼叫與日誌記錄訊息的級別相關聯的方法(例如 severe() 等)。
自定義異常中可以新增自定義域以滿足需求。


12.5 異常說明

異常說明是屬於方法宣告的一部分,緊跟在形式引數列表之後。Java 中強制使用這種語法,以告知使用該方法的人可能丟擲的異常型別。使用關鍵字 throws,後面接一個所有潛在異常型別的列表。
但是,對於從 RuntimeException 繼承的異常,可以在沒有異常說明的情況下被丟擲。

宣告方法的時候可以丟擲異常,實際上卻不丟擲。編譯器會相信這個宣告,並強制此方法的使用者像真的丟擲異常那樣使用這個方法。


12.6 捕獲所有異常

catch(rException e) {
    // handle exception
}

使用上面的方式可以捕獲所有的異常。

呼叫從基類 Throwable 繼承的方法:

  • String getMessage() 用來獲取異常的詳細資訊;
  • 使用 String getLocalizedMessage() 獲取本地語言表示的詳細資訊
  • 使用 toString() 獲取對 Throwable 的簡單描述,要是有詳細資訊

列印 ThrowableThrowable 的呼叫棧軌跡,呼叫棧顯示了“把你帶到異常丟擲地點”的方法呼叫序列:

  • void printStackTrace() 輸出到標準錯誤
  • void printStackTrace(PrintStream) 輸出到流中
  • void printStackTrace(java.io.PrintWriter) 輸出到流中

Throwable fillInStackTrace() 用於在 Throwable 物件的內部記錄棧幀的當前狀態。
getClass() 也可以用來獲取異常物件的名稱等屬性。

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


可以使用 throw e 將在 catch 中捕獲到的異常重新跑出去。
當只是把當前異常物件重新丟擲時,printStackTrace() 方法顯示的將是原來異常丟擲點的呼叫棧資訊,而不是重新丟擲點的資訊。
想要更新這個資訊,可以呼叫 fillInStackTrace() 方法,這將會返回一個 Throwable 物件,這個物件是通過把當前呼叫棧資訊填入原來那個異常物件而建立的(即呼叫 fillInStackTrace() 方法處就是異常的新發生地)。


在捕獲一個異常後丟擲另一個異常,並且希望把原始異常的資訊儲存下來,這被稱作異常鏈
Throwable 的子類在構造器中都可以接受一個cause(因由)物件作為引數。這個 cause 就用來表示原始異常,這樣通過把原始異常傳遞給新的異常,使得即使在當前位置建立並丟擲了新的異常,也能通過這個異常鏈追蹤到異常最初發生過的位置。

Throwable 的子類中,只有三種基本的異常類提供了帶 cause 引數的構造器--ErrorExceptionRuntimeException。如果要把其他型別的異常連結起來,應該使用 initCause() 方法而不是構造器。
(個別細節不清楚,再看一遍)


12.7 Java 標準異常

Throwable 被用來表示任何可以被作為異常丟擲的類。

Throwable 物件可以分為兩種型別(指從 Throwable 繼承而得到的型別):

  • Error 用來表示編譯時和系統錯誤(除特殊情況外,一般不用關心)
  • Exception 是可以被丟擲的基本型別。

異常的基本概念是用名稱代表發生的問題(望文知意)。


特例:RuntimeException 異常
對於 RuntimeException 以及其他繼承他的異常類,他們不需要在異常說明中宣告方法將丟擲 RuntimeException 型別異常(或任何從 RuntimeException 繼承的異常,因此也被稱為“不受檢查的異常”。這種異常屬於錯誤,將被系統自動捕獲,不需要親自處理-->其代表的是程式設計錯誤:

  • 無法預料的錯誤,比如從控制範圍之外傳遞進來的 null 引用。
  • 作為程式設計師,應該在程式碼中進行檢查的錯誤。

對於 RuntimeException 這種異常型別,編譯器不需要異常說明,其輸出將被直接報告給 System.error


12.8 使用 finally 進行清理

如果希望一段程式碼,無論 try 塊中的異常是否被丟擲,都能得到執行,name 可以使用 finally 子句。
用到 finally 的幾種情況:

  1. 把記憶體之外的資源恢復到他們的初始狀態,包括:已經開啟的檔案、網路連線、外部開關、圖形等
  2. 在異常沒有被當前的異常處理程式捕獲的情況下,需要執行一些程式碼
  3. 涉及 break 和 continue 語句時,也需要執行的程式碼
  4. 即使在 try 塊中 return 之後,也需要執行的程式碼

在 try 塊中使用 try...finally... 結構會導致意外的異常丟失,這是 Java 異常實現的缺憾(針對 Java SE6之前版本)


12.9 異常的限制

在覆蓋方法的時候,只能丟擲在基類方法的異常說明裡列出的那些異常。-->這個限制意味著,當基類使用的程式碼應用到其派生類物件的時候,一樣能夠工作(包括異常也能工作)。

針對構造器以及繼承或實現的方法有幾點:

  • 異常限制對構造器不起作用,構造器可以丟擲任何異常,而不必理會基類構造器所丟擲的異常。
  • 然而基類構造器必須以這樣或那樣的方式被呼叫(這裡預設構造器將自動被呼叫)後,派生類構造器的異常說明就必須包含基類構造器的異常說明。
  • 派生類構造器不能捕獲基類構造器丟擲的異常(意味著只能丟擲)。通過強制派生類遵守基類方法的異常說明,物件的可替換性得到了保證。
  • 派生類的方法可以選擇不丟擲任何異常,即使它是基類所定義的異常。
  • 使用派生類時,編譯器只會強制要求捕獲該派生類所丟擲的異常;但是如果將其向上轉型,那麼編譯器就會要求捕獲基類丟擲的異常。
  • 異常說明本身不屬於方法型別的一部分,方法型別是由方法的名字與引數的型別組成的。因此不能基於異常說明來過載方法。
  • 一個出現在基類方法的異常說明中的異常,不一定會出現在炮聲類方法的異常說明裡。與繼承中,基類的方法必須出現在派生類裡的這種方法相比較,在繼承和覆蓋的過程中,某個特定方法的“異常說明的介面”是變小了的,與類方法的繼承正好相反

12.10 構造器

對於構造器被呼叫時產生的異常,如果簡單的使用 try...catch...finally 結構來處理異常,容易丟失掉異常,並且不能完成finally 內的程式碼邏輯,或是在不希望的情況下去完成了 finally 下的邏輯。對於這種情況,需要再用一層 try...catch 來捕獲這個容易丟失的異常。
如下:

public static void main(String[] args) {
    try {
        InputFile in = new InputFile("Cleanup.java");
        try {
            String s;
            int i = 1;
            while ((s = in.getLine()) != null) {
                // Perform line-by-line processing here...
            }
        } catch (Exception e) {
            System.out.println("Caught Exception in main");
            e.printStackTrace(System.out);
        } finally {
            in.dispose();
        }
    } catch (Exception e) {
        System.out.println("InputFile construction failed");
    }
}

在構造之後以及建立一個新的 try 塊,將構造與其他可能丟擲異常的邏輯區分開,這樣不會讓 finally 內的邏輯被意外執行。
這種方式的基本規則是,在建立需要清理的物件之後,立即進入一個 try...finally 語句塊。

總之在建立構造器的時候,如果容易產生異常,應該仔細考慮如何處理構造器的異常。


12.11 異常匹配

丟擲異常的時候,異常處理系統會按照程式碼的書寫順序找出“最近”的處理程式。找到匹配的處理程式之後,他就認為異常將得到處理,然後就不再繼續查詢。

查詢的時候並不要求丟擲的異常和處理程式所宣告的異常完全匹配。派生類的物件也可以匹配其基類的處理程式。
catch(xxxException e) 會捕獲 xxxException 以及所有從他派生的異常。也就是說,如果捕獲基類異常,那麼在方法內加上更多派生的異常的時候,就無需更改程式。

如果把捕獲基類的 catch 子句放在最前面,然後將後面放上派生類異常的 catch 子句,那麼編譯器會發現無法捕獲派生類的異常,然後就會報錯。


12.12 其他可選方式

異常,代表了當前方法不能繼續執行的情形。異常處理系統就像一個活門(trap door),使人可以放棄程式的正常執行序列。當異常情形發生的時候,正常的執行已經不重要了,這個時候就要用到這個“活門”。
異常處理的重要原則:只有在你知道如何處理的情況下才捕獲異常。
異常處理的一個重要目標就是把錯誤處理的程式碼通錯誤發生的地點相分離。

“吞食則有害”(harmful if swallowed):這個概念,是由在沒準備好處理錯誤的情況下,添加了 catch 子句,這樣異常雖然可能已經發生了,但是編譯器並未顯示錯誤,異常好像消失了。

後面介紹了一些異常的發展史。。。看的想去念研究生。。。


12.13 異常使用指南

總結起來,應該在以下情況使用異常:

  1. 在恰當的級別處理問題。(在知道該如何處理的情況下才捕獲異常)
  2. 解決問題並且重新呼叫產生異常的方法。
  3. 進行少許修補,然後繞過異常發生的地方繼續執行。
  4. 用別的資料進行計算,以代替方法預計會返回的值。
  5. 把當前執行環境下能做的事情儘量做完,然後把相同的異常拋到更高層。
  6. 把當前執行環境下能做的事情儘量做完,然後把不同的異常拋到更高層。
  7. 終止程式。
  8. 進行簡化。(如果異常模式使問題變得太複雜,那麼會很難使用)
  9. 讓類庫和程式更安全。(這既是在為除錯做短期投資,也是為程式的健壯性做長期投資)