1. 程式人生 > 實用技巧 >Java面向物件程式設計-異常處理

Java面向物件程式設計-異常處理

第九章 異常處理

異常情況會改變正常的流程,導致惡劣的後果,為了減少損失,應該事先充分預料所有可能出現的異常,然後採取以下措施:

  • 首先考慮避免異常,徹底杜絕異常的發生;如果不能完全避免,則儘可能地減少異常的發生的機率。
  • 如果有些異常不可避免,那麼應該預先準備好處理異常的措施,從而降低或彌補異常造成的損失,或者恢復正常的流程。
  • 對於某個系統遇到的異常,有些異常單靠系統本身就能處理,有些異常需要系統本身及其它系統共同處理。
  • 對於某個系統遇到的異常,系統本身應該儘可能地處理異常,實在沒辦法處理,才求助於其它系統處理。

Java系統提供了一套完善的異常處理機制。正確的運用這套機制,有助於提高程式的健壯性。所謂健壯性是指程式在多數情況下能夠正常執行,返回預期的結果,如果偶爾遇到異常情況,程式也能採取周到的解救措施。

9.1 Java異常處理機制概述

在處理異常中要考慮的兩個問題:

  1. 如何表示異常情況
  2. 如何控制處理異常的流程
9.1.1 Java異常處理機制的優點

傳統的異常處理方式儘管是有效的,但存在以下缺點:

  • 表示異常情況的能力有限,單靠方法的返回值難以表達異常情況包含的所有資訊。
  • 異常流程的程式碼和正常流程的程式碼混合在一起,影響程式的可讀性,容易增加程式結構的複雜性。
  • 隨著系統規模的不斷擴大,這種處理方式已經成為建立大型可維護專案的障礙。

Java語言按照面向物件的思想來處理異常,使得程式具有更好的可維護性。Java異常處理機制具有以下優點:

  • 把各種不同型別的異常情況進行分類,用Java類表示異常情況,這種類被稱為異常類。把異常情況標識成異常類,可以充分發揮類的可擴充套件性和可重用的優勢。
  • 異常流程的程式碼和正常流程的程式碼分離,提高了程式的可讀性。簡化了程式的結構。
  • 可以靈活地處理異常,如果當前方法有能力處理異常,就捕獲並處理它,否則只需丟擲異常,由方法呼叫者來處理。
9.1.2 Java虛擬機器的方法呼叫棧

Java虛擬機器使用方法呼叫棧來跟蹤每個執行緒種一系列的方法呼叫過程。該堆疊中儲存了每個呼叫方法的本地資訊(比如方法的區域性變數)。每個執行緒都有獨立的方法呼叫棧。對於方法Java應用程式的主執行緒,堆疊底部是程式的入口方法main(),當一個方法被呼叫時,Java虛擬機器把描述該方法的棧結構置入棧頂部,位於棧頂的方法為正在執行的方法。

如果方法中的程式碼塊可能丟擲異常,有兩種處理方法:

  1. 在當前方法中通過try-catch語句捕獲並處理異常。
  2. 在方法的宣告處通過throws語句宣告丟擲異常。

當一個方法正常執行完畢後,Java虛擬機器會從呼叫棧中彈出該方法的棧結構。然後繼續處理前一個方法,如果在執行方法的過程中丟擲異常,Java虛擬機器必須找到能夠捕獲該異常的catch程式碼塊。(首先檢視當前方法中是否存在這樣的catch程式碼塊,如果存在就執行該語句,否則,Java虛擬機器會從呼叫棧中彈出該方法,繼續到前一個方法中尋找合適的catch程式碼塊。)

當Java虛擬機器追溯到呼叫棧的最底部,如果仍然沒有找到處理該異常的程式碼塊,將按照以下步驟執行:

  • 呼叫異常物件的printStackTrace()方法,列印來自方法呼叫棧的異常資訊。
  • 如果該執行緒不是主執行緒,那麼終止這個執行緒,其它執行緒繼續正常執行。如果該執行緒是主執行緒(即方法呼叫棧的最底部是main()方法),那麼整個應用程式會被終止。
9.1.3 異常處理對效能的影響

一般來說,在Java程式中使用try-catch語句不會對應用的效能造成很大的影響。僅當異常發生時,Java虛擬機器需要進行額外的操作,來定位處理異常的程式碼塊,此時會對效能產生負面影響。如果丟擲異常的程式碼塊和捕獲異常的程式碼塊位於同一個方法中,那麼這種影響要小一些;如果Java虛擬機器必須搜尋方法的呼叫棧來尋找異常處理程式碼塊,對效能的影響就比較大了。

所以,應該確保僅僅在程式中可能出現異常的地方使用try-catch語句,此外,應該使異常處理程式碼塊位於適當的層次。如果當前方法具備某種處理異常的能力,就儘量自行處理,不要把自己可以處理的異常推給方法的呼叫者去處理。

9.2 運用Java異常處理機制

9.2.1 try-catch語句

在Java語言中,用try-catch語句進行異常處理:

try{
    可能會出現異常情況的程式碼
}catch (SQLException e){
    處理操作資料庫出現的異常
}catch(IOException e){
    處理操作輸入輸出流出現的異常
}
9.2.2 finally語句:任何情況下必須執行的程式碼

finally程式碼塊能夠保證特定的操作總是會執行。它的形式如下:

public void work() throws LeaveEarlyException{
    try{
        開門
            工作8小時
    }catch(DiseaseException e){
        throw new LeaveEarlyException();
    }finally{
        關門
    }
}

不管try程式碼塊中是否出現異常,都會執行finally程式碼塊。

這種處理方式在某些情況下是可行的,但不值得推薦,因為它有兩個缺點:

  • 把與try程式碼塊相關的操作孤立開來,使程式結構鬆散,可讀性差。
  • 影響程式的健壯性。
9.2.3 throws子句:宣告可能會出現的異常

如果一個方法可能會出現異常,但沒有能力處理這種異常,可以在方法的宣告處用throws子句來宣告丟擲異常。

一個方法可能出現多種異常,throws子句允許宣告丟擲多個異常,例如:

public void method() throws SQLException, IOException{... ...}

異常宣告是介面的一部分,在JavaDoc文件中應描述方法可能丟擲的異常。

9.2.4 throw語句:丟擲異常

throw語句用於丟擲異常,有throw語句丟擲的物件必須是java.lang.Throwable類或者其子類的例項。

9.2.5 異常處理語句的語法規則
  • try程式碼塊後面可以有零個或多個catch程式碼塊,還可以有零個或至多一個finally程式碼塊。如果catck程式碼塊和finally程式碼塊同時存在,finally程式碼塊必須在catch程式碼塊後面。
  • try程式碼塊後面可以只跟finally程式碼塊。
  • 在try程式碼塊中定義的變數的作用域為try程式碼塊,在catch程式碼塊和finally程式碼塊中不能訪問該變數。
  • 當try程式碼塊後面有多個catch程式碼塊時,Java虛擬機器會把實際丟擲異常物件依次和各個catch程式碼塊宣告的異常型別匹配,如果異常物件為某個異常型別或其子類的例項,就執行這個程式碼塊,不會再執行其它的catch程式碼塊。
  • 為了簡化程式設計,從JDK7開始,允許在一個catch子句中同時捕獲多個不同型別的異常,用|符合進行分隔。
  • 如果一個方法可能出現受檢查異常,要麼用try-catch語句捕獲,要麼用throws子句宣告將它丟擲,否則會導致編譯錯誤。
    • 判斷一個方法可能會出現異常的依據如下:
      • 方法中有throw語句。
      • 呼叫了其它方法,其它方法用throws子句宣告丟擲某種異常。
  • 針對前一條語法規則,從JDK7開始,如果在catch子句中捕獲的異常被宣告為final型別,那麼當catch子句中繼續丟擲該異常時,可以不用在定義方法時用throws子句宣告將它丟擲。
  • throw語句後面不允許緊跟其它語句,因為這些語句永遠不會被執行。
9.2.6 異常流程的執行過程
  • finally語句不被執行的唯一情況是:先執行了用於終止程式的System.exit()方法。java.lang.System類的靜態方法exit()用於終止當前Java虛擬機器程序,Java虛擬機器所執行的Java程式也會隨之停止。exit()方法的定義如下:
public static void exit(int status)
  • return 語句用於退出本方法。在執行try或catch程式碼塊中的return語句時,加入有finally程式碼塊,會先執行finally程式碼塊。
  • funally程式碼塊雖然在return語句之前被執行,但finally程式碼塊不能通過重新該變數賦值的方式來改變return語句的返回值。
  • 建議不要在finally程式碼塊中使用return語句,因為他會導致以下兩種潛在的錯誤:
    • 覆蓋try或catch程式碼塊中的return語句。
    • 丟失異常。
9.2.7 跟蹤丟失的異常

在JDK7中,Throwable介面中增加了兩個已經提供的預設實現的方法

public final void addSuppressed(Throwable exception)
public final Throwable[] getSuppressed()

以上add方法把差點丟失的異常儲存起來,get方法返回所儲存下來的差點丟失的異常。

9.3 Java異常類

Java中,所有異常類的祖先類為java.lang.Throwable類。它的例項表示具體的異常型別,可以使用throw語句丟擲。Throwable類提供了訪問異常資訊的一些方法,常用的方法如下:

  • getMessage():返回String型別的異常資訊
  • printStackTrace():列印跟蹤方法呼叫棧而獲得的詳細異常資訊。

Throwable類有兩個直接子類:

  • Error類:表示單靠程式本身無法恢復的嚴重錯誤,比如記憶體空間不足,或者Java虛擬機器的方法呼叫棧溢位。在大多數情況下,遇到這種錯誤時,建議讓程式終止。
  • Exception類:表示程式本身可以處理的異常,當程式執行時出現這類異常,應該儘可能地處理異常,並且使程式恢復執行,而不應該隨意終止程式。

JDK中定義了一些具體的異常:

  • IOException:操作輸入流和輸出流是可能出現的異常。
  • ArithmeticException:數學異常。如把整數除以0,就會出現這種異常。
  • NullPointerException:空指標異常。當引用變數為null時,試圖訪問物件的屬性或方法,就會出現這種異常。
  • IndexOutOfBoundsException:下標越界異常。它的子類ArrayIndexOutOfBoundsException表示陣列下標越界異常。
  • ClassCastException:型別轉換異常。
  • IllegalArgumentException:非法引數異常,可用來檢查方法的引數是否合法。
9.3.1 執行時異常

RuntimeException類及其子類都被稱為執行時異常,這種異常的特點是Java編譯器不會檢查它,也就是說,當程式中可能出現這類異常時,即使沒有使用try-catch語句捕獲它,也沒有用throws子句宣告丟擲它,也會編譯通過。

由於程式程式碼不會處理執行時異常,因此當程式在執行時出現了這種異常時,就會導致程式異常終止。

9.3.2 受檢查時異常

除了RuntimeException及其子類以為,其它的Exception類及其子類都屬於受檢查異常。這種異常的特點是Java虛擬機器會檢查它,也就是說,當程式中可能出現這種異常時,要麼使用try-catch語句捕獲它,要麼使用throws子句宣告丟擲它,否則編譯不會通過。

9.3.3 區分執行時異常和受檢查異常

受檢查異常表示程式可以處理的異常,如果丟擲異常的方法本身不能處理它,那麼方法的呼叫者應該可以處理它,從而使程式恢復執行,不至於終止程式。

執行時異常表示無法讓程式恢復執行的異常,導致這種異常的原因通常是執行了錯誤操作。一旦出現了錯誤操作,建議終止程式,因此Java虛擬機器不檢查這種錯誤。

9.4 使用者定義異常

在特定的問題領域,可以通過擴充套件Exception類或RuntimeException類來建立自定義的異常,異常類包含和異常相關的資訊。

9.4.1 異常轉譯和異常鏈
9.4.2 處理多樣化異常

9.5 異常處理原則

9.5.1 異常只能用於非正常情況

這種處理方式有以下缺點:

  • 濫用異常流程會降低程式的效能
  • 用異常類來表示正常情況,違背了異常處理機制的初衷。
  • 模糊了程式程式碼的意圖,影響可讀性。
  • 容易掩飾程式程式碼中的錯誤,增加除錯的複雜性。
9.5.2 為異常提供說明文件
9.5.3 儘可能地避免異常
  1. 許多執行時異常是由於程式程式碼中的錯誤引起的,只有修改了程式程式碼的錯誤,或者改進了程式的實現方式,就能避免這種錯誤。
  2. 提供狀態測試方法。有些異常是由於當物件處於某種狀態下,不適合某種操作造成的。
9.5.4 保持異常的原子性

異常的原子性是指當異常發生後,各個物件的狀態能夠恢復到異常發生前的初始狀態。而不至於停留在某個不合理的中間狀態。物件的狀態是否合理,是由特定問題領域的業務決定的。

保持異常的原子性有以下辦法:

  • 先檢查方法的引數是否有效,確保當異常發生時還沒有改變物件的初始狀態。
  • 編寫一段恢復程式碼,由它來解釋操作過程中發生的失敗,並且使物件狀態回滾到初始狀態。
  • 在物件的臨時副本上進行操作,當操作成功後,把臨時副本中的內容複製到原來的物件中。
9.5.5 避免龐大的try程式碼塊
9.5.6 在catch子句中指定具體的異常型別。
9.5.7 不要在catch程式碼塊中忽略被捕獲的異常

9.6 記錄日誌

輸出日誌的作用:

  • 監視程式碼中的變數的變化情況,把資料週期性的記錄到檔案中供其它應用進行統計分析工作。
  • 跟蹤程式碼執行時軌跡,作為日後審計的依據。
  • 承擔整合開發環境中的偵錯程式的作用,向檔案或控制檯列印程式碼的除錯資訊。

可以直接使用Java類庫中的java.util.logging日誌操作包。這個包中主要有4個類:

  • Logger類:負責生成日誌,並能夠對日誌資訊進行分級別篩選,通俗地講,就是決定什麼級別的日誌資訊應該被輸出,什麼級別的日誌應該被忽略。
  • Handler類:負責輸出日誌資訊,它有兩個子類:ConcoleHandler類(把日誌輸出到DOS命令列控制檯)、和FileHandler類(把日誌輸出到檔案中)
  • Formatter類:指定日誌資訊的輸出格式。它有兩個子類:SimpleFormatter類(常用的日誌格式)和XMLFormatter類(表示基於XML的日誌格式)
  • Level類:表示日誌的各種級別,它的靜態常量表示不同的日誌級別。
9.6.1 建立Logger物件及設定日誌級別

首先通過Logger類的getLogger(String name)方法獲得一個Logger物件

Logger myLogger = Logger.getLogger("myLogger");

在預設情況下,Logger類只輸出SEVERE,WARNING, INFO這前三個級別。可以通過Logger類的setLevel()方法設定日誌級別。

logger.setLevel("Level.FINE"); // 把日誌級別設為FINE
logger.setLevel("Level.WARNING"); // 把日誌級別設為WARNING
logger.setLevel("Level.ALL") // 開啟所有日誌級別
logger.setLevel("Level.OFF") // 關閉所有日誌級別
9.6.2 生成日誌

Logger類的severe(), warn(), info()方法等分別生成各級級別的日誌。

9.6.3 把日誌輸出到檔案

預設情況下,Logger類把日誌輸出到DOS控制檯

9.6.4 設定日誌的輸出格式

9.7 使用斷言

格式:

assert 條件表示式
assert 條件表示式:包含錯誤資訊的表示式

當表示式的值為false時,就會丟擲一個AssertError,第二種形式中,第二個表示式會被轉換成錯誤訊息的字串。

當程式執行時,斷言在預設情況下是關閉的。