Skip to content
This repository has been archived by the owner on Aug 25, 2019. It is now read-only.

Latest commit

 

History

History
1090 lines (841 loc) · 57.2 KB

第9章.md

File metadata and controls

1090 lines (841 loc) · 57.2 KB

第9章 異常差錯控制

Java的基本原理就是“形式錯誤的代碼不會運行”。

與C++類似,捕獲錯誤最理想的是在編譯期間,最好在試圖運行程序以前。然而,並非所有錯誤都能在編譯期間偵測到。有些問題必須在運行期間解決,讓錯誤的締結者通過一些手續向接收者傳遞一些適當的信息,使其知道該如何正確地處理遇到的問題。

在C++和其他早期語言中,可通過幾種手續來達到這個目的。而且它們通常是作為一種規定建立起來的,而非作為程序設計語言的一部分。典型地,我們需要返回一個值或設置一個標誌(位),接收者會檢查這些值或標誌,判斷具體發生了什麼事情。然而,隨著時間的流逝,終於發現這種做法會助長那些使用一個庫的程序員的麻痺情緒。他們往往會這樣想:“是的,錯誤可能會在其他人的代碼中出現,但不會在我的代碼中”。這樣的後果便是他們一般不檢查是否出現了錯誤(有時出錯條件確實顯得太愚蠢,不值得檢驗;註釋①)。另一方面,若每次調用一個方法時都進行全面、細緻的錯誤檢查,那麼代碼的可讀性也可能大幅度降低。由於程序員可能仍然在用這些語言維護自己的系統,所以他們應該對此有著深刻的體會:若按這種方式控制錯誤,那麼在創建大型、健壯、易於維護的程序時,肯定會遇到不小的阻撓。

①:C程序員研究一下printf()的返回值便知端詳。

解決的方法是在錯誤控制中排除所有偶然性,強制格式的正確。這種方法實際已有很長的歷史,因為早在60年代便在操作系統裡採用了“異常控制”手段;甚至可以追溯到BASIC語言的on error goto語句。但C++的異常控制建立在Ada的基礎上,而Java又主要建立在C++的基礎上(儘管它看起來更象Object Pascal)。

“異常”(Exception)這個詞表達的是一種“例外”情況,亦即正常情況之外的一種“異常”。在問題發生的時候,我們可能不知具體該如何解決,但肯定知道已不能不顧一切地繼續下去。此時,必須堅決地停下來,並由某人、某地指出發生了什麼事情,以及該採取何種對策。但為了真正解決問題,當地可能並沒有足夠多的信息。因此,我們需要將其移交給更級的負責人,令其作出正確的決定(類似一個命令鏈)。

異常機制的另一項好處就是能夠簡化錯誤控制代碼。我們再也不用檢查一個特定的錯誤,然後在程序的多處地方對其進行控制。此外,也不需要在方法調用的時候檢查錯誤(因為保證有人能捕獲這裡的錯誤)。我們只需要在一個地方處理問題:“異常控制模塊”或者“異常控制器”。這樣可有效減少代碼量,並將那些用於描述具體操作的代碼與專門糾正錯誤的代碼分隔開。一般情況下,用於讀取、寫入以及調試的代碼會變得更富有條理。

由於異常控制是由Java編譯器強行實現的,所以毋需深入學習異常控制,便可正確使用本書編寫的大量例子。本章向大家介紹了用於正確控制異常所需的代碼,以及在某個方法遇到麻煩的時候,該如何生成自己的異常。

9.1 基本異常

“異常條件”表示在出現什麼問題的時候應中止方法或作用域的繼續。為了將異常條件與普通問題區分開,異常條件是非常重要的一個因素。在普通問題的情況下,我們在當地已擁有足夠的信息,可在某種程度上解決碰到的問題。而在異常條件的情況下,卻無法繼續下去,因為當地沒有提供解決問題所需的足夠多的信息。此時,我們能做的唯一事情就是跳出當地環境,將那個問題委託給一個更高級的負責人。這便是出現異常時出現的情況。

一個簡單的例子是“除法”。如可能被零除,就有必要進行檢查,確保程序不會冒進,並在那種情況下執行除法。但具體通過什麼知道分母是零呢?在那個特定的方法裡,在我們試圖解決的那個問題的環境中,我們或許知道該如何對待一個零分母。但假如它是一個沒有預料到的值,就不能對其進行處理,所以必須產生一個異常,而非不顧一切地繼續執行下去。

產生一個異常時,會發生幾件事情。首先,按照與創建Java對象一樣的方法創建異常對象:在內存“堆”裡,使用new來創建。隨後,停止當前執行路徑(記住不可沿這條路徑繼續下去),然後從當前的環境中釋放出異常對象的引用。此時,異常控制機制會接管一切,並開始查找一個恰當的地方,用於繼續程序的執行。這個恰當的地方便是“異常控制器”,它的職責是從問題中恢復,使程序要麼嘗試另一條執行路徑,要麼簡單地繼續。

作為產生異常的一個簡單示例,大家可思考一個名為t的對象引用。有些時候,程序可能傳遞一個尚未初始化的引用。所以在用那個對象引用調用一個方法之前,最好進行一番檢查。可將與錯誤有關的信息發送到一個更大的場景中,方法是創建一個特殊的對象,用它代表我們的信息,並將其“拋”(Throw)出我們當前的場景之外。這就叫作“產生一個異常”或者“拋出一個異常”。下面是它的大概形式:

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

這樣便“拋”出了一個異常。在當前場景中,它使我們能放棄進一步解決該問題的企圖。該問題會被轉移到其他更恰當的地方解決。準確地說,那個地方不久就會顯露出來。

9.1.1 異常參數

和Java的其他任何對象一樣,需要用new在內存堆裡創建異常,並需調用一個構造器。在所有標準異常中,存在著兩個構造器:第一個是默認構造器,第二個則需使用一個字符串參數,使我們能在異常裡置入相關信息:

if(t == null)
throw new NullPointerException("t = null");

稍後,字符串可用各種方法提取出來,就象稍後會展示的那樣。

在這兒,關鍵字throw會象變戲法一樣做出一系列不可思議的事情。它首先執行new表達式,創建一個不在程序常規執行範圍之內的對象。而且理所當然,會為那個對象調用構造器。隨後,對象實際會從方法中返回——儘管對象的類型通常並不是方法設計為返回的類型。為深入理解異常控制,可將其想象成另一種返回機制——但是不要在這個問題上深究,否則會遇到麻煩。通過“拋”出一個異常,亦可從原來的作用域中退出。但是會先返回一個值,再退出方法或作用域。

但是,與普通方法返回的相似性到此便全部結束了,因為我們返回的地方與從普通方法調用中返回的地方是迥然有異的(我們結束於一個恰當的異常控制器,它距離異常“拋”出的地方可能相當遙遠——在調用棧中要低上許多級)。

此外,我們可根據需要拋出任何類型的“可拋”對象。典型情況下,我們要為每種不同類型的錯誤“拋”出一類不同的異常。我們的思路是在異常對象以及挑選的異常對象類型中保存信息,所以在更大場景中的某個人可知道如何對待我們的異常(通常,唯一的信息是異常對象的類型,而異常對象中保存的沒什麼意義)。

9.2 異常的捕獲

若某個方法產生一個異常,必須保證該異常能被捕獲,並獲得正確對待。對於Java的異常控制機制,它的一個好處就是允許我們在一個地方將精力集中在要解決的問題上,然後在另一個地方對待來自那個代碼內部的錯誤。

為理解異常是如何捕獲的,首先必須掌握“警戒區”的概念。它代表一個特殊的代碼區域,有可能產生異常,並在後面跟隨用於控制那些異常的代碼。

9.2.1 try

若位於一個方法內部,並“拋”出一個異常(或在這個方法內部調用的另一個方法產生了異常),那個方法就會在異常產生過程中退出。若不想一個throw離開方法,可在那個方法內部設置一個特殊的代碼塊,用它捕獲異常。這就叫作“try塊”,因為要在這個地方“嘗試”各種方法調用。try塊屬於一種普通的作用域,用一個try關鍵字開頭:

try {
// 可能產生異常的代碼
}

若用一種不支持異常控制的編程語言全面檢查錯誤,必須用設置和錯誤檢測代碼將每個方法都包圍起來——即便多次調用相同的方法。而在使用了異常控制技術後,可將所有東西都置入一個try塊內,在同一地點捕獲所有異常。這樣便可極大簡化我們的代碼,並使其更易辨讀,因為代碼本身要達到的目標再也不會與繁複的錯誤檢查混淆。

9.2.2 異常控制器

當然,生成的異常必須在某個地方中止。這個“地方”便是異常控制器或者異常控制模塊。而且針對想捕獲的每種異常類型,都必須有一個相應的異常控制器。異常控制器緊接在try塊後面,且用catch(捕獲)關鍵字標記。如下所示:

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
}

// etc...

每個catch從句——即異常控制器——都類似一個小型方法,它需要採用一個(而且只有一個)特定類型的參數。可在控制器內部使用標識符(id1id2等等),就象一個普通的方法參數那樣。我們有時也根本不使用標識符,因為異常類型已提供了足夠的信息,可有效處理異常。但即使不用,標識符也必須就位。

控制器必須“緊接”在try塊後面。若“拋”出一個異常,異常控制機制就會搜尋參數與異常類型相符的第一個控制器。隨後,它會進入那個catch從句,並認為異常已得到控制(一旦catch從句結束,對控制器的搜索也會停止)。只有相符的catch從句才會得到執行;它與switch語句不同,後者在每個case後都需要一個break命令,防止誤執行其他語句。 在try塊內部,請注意大量不同的方法調用可能生成相同的異常,但只需要一個控制器。

(1) 中斷與恢復

在異常控制理論中,共存在兩種基本方法。在“中斷”方法中(Java和C++提供了對這種方法的支持),我們假定錯誤非常關鍵,沒有辦法返回異常發生的地方。無論誰只要“拋”出一個異常,就表明沒有辦法補救錯誤,而且也不希望再回來。

另一種方法叫作“恢復”。它意味著異常控制器有責任來糾正當前的狀況,然後取得出錯的方法,假定下一次會成功執行。若使用恢復,意味著在異常得到控制以後仍然想繼續執行。在這種情況下,我們的異常更象一個方法調用——我們用它在Java中設置各種各樣特殊的環境,產生類似於“恢復”的行為(換言之,此時不是“拋”出一個異常,而是調用一個用於解決問題的方法)。另外,也可以將自己的try塊置入一個while循環裡,用它不斷進入try塊,直到結果滿意時為止。

從歷史的角度看,若程序員使用的操作系統支持可恢復的異常控制,最終都會用到類似於中斷的代碼,並跳過恢復進程。所以儘管“恢復”表面上十分不錯,但在實際應用中卻顯得困難重重。其中決定性的原因可能是:我們的控制模塊必須隨時留意是否產生了異常,以及是否包含了由產生位置專用的代碼。這便使代碼很難編寫和維護——大型系統尤其如此,因為異常可能在多個位置產生。

9.2.3 異常規範

在Java中,對那些要調用方法的客戶程序員,我們要通知他們可能從自己的方法裡“拋”出異常。這是一種有禮貌的做法,只有它才能使客戶程序員準確地知道要編寫什麼代碼來捕獲所有潛在的異常。當然,若你同時提供了源碼,客戶程序員甚至能全盤檢查代碼,找出相應的throw語句。但儘管如此,通常並不隨同源碼提供庫。為解決這個問題,Java提供了一種特殊的語法格式(並強迫我們採用),以便禮貌地告訴客戶程序員該方法會“拋”出什麼異常,令對方方便地加以控制。這便是我們在這裡要講述的“異常規範”,它屬於方法聲明的一部分,位於參數列表的後面。

異常規範採用了一個額外的關鍵字:throws;後面跟隨全部潛在的異常類型。因此,我們的方法定義看起來應象下面這個樣子:

void f() throws tooBig, tooSmall, divZero { //...

若使用下述代碼:

void f() [ // ...

它意味著不會從方法裡“拋”出異常(除類型為RuntimeException的異常以外,它可能從任何地方拋出——稍後還會詳細講述)。 但不能完全依賴異常規範——假若方法造成了一個異常,但沒有對其進行控制,編譯器會偵測到這個情況,並告訴我們必須控制異常,或者指出應該從方法裡“拋”出一個異常規範。通過堅持從頂部到底部排列異常規範,Java可在編譯期保證異常的正確性(註釋②)。

②:這是在C++異常控制基礎上一個顯著的進步,後者除非到運行期,否則不會捕獲不符合異常規範的錯誤。這使得C++的異常控制機制顯得用處不大。

我們在這個地方可採取欺騙手段:要求“拋”出一個並沒有發生的異常。編譯器能理解我們的要求,並強迫使用這個方法的用戶當作真的產生了那個異常處理。在實際應用中,可將其作為那個異常的一個“佔位符”使用。這樣一來,以後可以方便地產生實際的異常,毋需修改現有的代碼。

9.2.4 捕獲所有異常

我們可創建一個控制器,令其捕獲所有類型的異常。具體的做法是捕獲基類異常類型Exception(也存在其他類型的基礎異常,但Exception是適用於幾乎所有編程活動的基礎)。如下所示:

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

這段代碼能捕獲任何異常,所以在實際使用時最好將其置於控制器列表的末尾,防止跟隨在後面的任何特殊異常控制器失效。 對於程序員常用的所有異常類來說,由於Exception類是它們的基礎,所以我們不會獲得關於異常太多的信息,但可調用來自它的基類Throwable的方法:

String getMessage()

獲得詳細的消息。

String toString()

返回對Throwable的一段簡要說明,其中包括詳細的消息(如果有的話)。

void printStackTrace()
void printStackTrace(PrintStream)

打印出ThrowableThrowable的調用棧路徑。調用棧顯示出將我們帶到異常發生地點的方法調用的順序。

第一個版本會打印出標準錯誤,第二個則打印出我們的選擇流程。若在Windows下工作,就不能重定向標準錯誤。因此,我們一般願意使用第二個版本,並將結果送給System.out;這樣一來,輸出就可重定向到我們希望的任何路徑。

除此以外,我們還可從Throwable的基類Object(所有對象的基類型)獲得另外一些方法。對於異常控制來說,其中一個可能有用的是getClass(),它的作用是返回一個對象,用它代表這個對象的類。我們可依次用getName()toString()查詢這個Class類的名字。亦可對Class對象進行一些複雜的操作,儘管那些操作在異常控制中是不必要的。本章稍後還會詳細講述Class對象。

下面是一個特殊的例子,它展示了Exception方法的使用(若執行該程序遇到困難,請參考第3章3.1.2小節“賦值”):

//: ExceptionMethods.java
// Demonstrating the Exception Methods
package c09;

public class ExceptionMethods {
  public static void main(String[] args) {
    try {
      throw new Exception("Here's my Exception");
    } catch(Exception e) {
      System.out.println("Caught Exception");
      System.out.println(
        "e.getMessage(): " + e.getMessage());
      System.out.println(
        "e.toString(): " + e.toString());
      System.out.println("e.printStackTrace():");
      e.printStackTrace();
    }
  }
} ///:~

該程序輸出如下:

Caught Exception
e.getMessage(): Here's my Exception
e.toString(): java.lang.Exception: Here's my Exception
e.printStackTrace():
java.lang.Exception: Here's my Exception
        at ExceptionMethods.main

可以看到,該方法連續提供了大量信息——每類信息都是前一類信息的一個子集。

9.2.5 重新“拋”出異常

在某些情況下,我們想重新拋出剛才產生過的異常,特別是在用Exception捕獲所有可能的異常時。由於我們已擁有當前異常的引用,所以只需簡單地重新拋出那個引用即可。下面是一個例子:

catch(Exception e) {
System.out.println("一個異常已經產生");
throw e;
}

重新“拋”出一個異常導致異常進入更高一級環境的異常控制器中。用於同一個try塊的任何更進一步的catch從句仍然會被忽略。此外,與異常對象有關的所有東西都會得到保留,所以用於捕獲特定異常類型的更高一級的控制器可以從那個對象裡提取出所有信息。

若只是簡單地重新拋出當前異常,我們打印出來的、與printStackTrace()內的那個異常有關的信息會與異常的起源地對應,而不是與重新拋出它的地點對應。若想安裝新的棧跟蹤信息,可調用fillInStackTrace(),它會返回一個特殊的異常對象。這個異常的創建過程如下:將當前棧的信息填充到原來的異常對象裡。下面列出它的形式:

//: Rethrowing.java
// Demonstrating fillInStackTrace()

public class Rethrowing {
  public static void f() throws Exception {
    System.out.println(
      "originating the exception in f()");
    throw new Exception("thrown from f()");
  }
  public static void g() throws Throwable {
    try {
      f();
    } catch(Exception e) {
      System.out.println(
        "Inside g(), e.printStackTrace()");
      e.printStackTrace();
      throw e; // 17
      // throw e.fillInStackTrace(); // 18
    }
  }
  public static void
  main(String[] args) throws Throwable {
    try {
      g();
    } catch(Exception e) {
      System.out.println(
        "Caught in main, e.printStackTrace()");
      e.printStackTrace();
    }
  }
} ///:~

其中最重要的行號在註釋內標記出來。注意第17行沒有設為註釋行。它的輸出結果如下:

originating the exception in f()
Inside g(), e.printStackTrace()
java.lang.Exception: thrown from f()
        at Rethrowing.f(Rethrowing.java:8)
        at Rethrowing.g(Rethrowing.java:12)
        at Rethrowing.main(Rethrowing.java:24)
Caught in main, e.printStackTrace()
java.lang.Exception: thrown from f()
        at Rethrowing.f(Rethrowing.java:8)
        at Rethrowing.g(Rethrowing.java:12)
        at Rethrowing.main(Rethrowing.java:24)

因此,異常棧路徑無論如何都會記住它的真正起點,無論自己被重複“拋”了好幾次。 若將第17行標註(變成註釋行),而撤消對第18行的標註,就會換用fillInStackTrace(),結果如下:

originating the exception in f()
Inside g(), e.printStackTrace()
java.lang.Exception: thrown from f()
        at Rethrowing.f(Rethrowing.java:8)
        at Rethrowing.g(Rethrowing.java:12)
        at Rethrowing.main(Rethrowing.java:24)
Caught in main, e.printStackTrace()
java.lang.Exception: thrown from f()
        at Rethrowing.g(Rethrowing.java:18)
        at Rethrowing.main(Rethrowing.java:24)

由於使用的是fillInStackTrace(),第18行成為異常的新起點。

針對g()main()Throwable類必須在異常規約中出現,因為fillInStackTrace()會生成一個Throwable對象的引用。由於ThrowableException的一個基類,所以有可能獲得一個能夠“拋”出的對象(具有Throwable屬性),但卻並非一個Exception(異常)。因此,在main()中用於Exception的引用可能丟失自己的目標。為保證所有東西均井然有序,編譯器強制Throwable使用一個異常規範。舉個例子來說,下述程序的異常便不會在main()中被捕獲到:

//: ThrowOut.java
public class ThrowOut {
  public static void
  main(String[] args) throws Throwable {
    try {
      throw new Throwable();
    } catch(Exception e) {
      System.out.println("Caught in main()");
    }
  }
} ///:~

也有可能從一個已經捕獲的異常重新“拋”出一個不同的異常。但假如這樣做,會得到與使用fillInStackTrace()類似的效果:與異常起源地有關的信息會全部丟失,我們留下的是與新的throw有關的信息。如下所示:

//: RethrowNew.java
// Rethrow a different object from the one that
// was caught

public class RethrowNew {
  public static void f() throws Exception {
    System.out.println(
      "originating the exception in f()");
    throw new Exception("thrown from f()");
  }
  public static void main(String[] args) {
    try {
      f();
    } catch(Exception e) {
      System.out.println(
        "Caught in main, e.printStackTrace()");
      e.printStackTrace();
      throw new NullPointerException("from main");
    }
  }
} ///:~

輸出如下:

originating the exception in f()
Caught in main, e.printStackTrace()
java.lang.Exception: thrown from f()
        at RethrowNew.f(RethrowNew.java:8)
        at RethrowNew.main(RethrowNew.java:13)
java.lang.NullPointerException: from main
        at RethrowNew.main(RethrowNew.java:18)

最後一個異常只知道自己來自main(),而非來自f()。注意Throwable在任何異常規範中都不是必需的。

永遠不必關心如何清除前一個異常,或者與之有關的其他任何異常。它們都屬於用new創建的、以內存堆為基礎的對象,所以垃圾收集器會自動將其清除。

9.3 標準Java異常

Java包含了一個名為Throwable的類,它對可以作為異常“拋”出的所有東西進行了描述。Throwable對象有兩種常規類型(亦即“從Throwable繼承”)。其中,Error代表編譯期和系統錯誤,我們一般不必特意捕獲它們(除在特殊情況以外)。Exception是可以從任何標準Java庫的類方法中“拋”出的基本類型。此外,它們亦可從我們自己的方法以及運行期偶發事件中“拋”出。

為獲得異常的一個綜合概念,最好的方法是閱讀由http://java.sun.com提供的聯機Java文檔(當然,首先下載它們更好)。為了對各種異常有一個大概的印象,這個工作是相當有價值的。但大家不久就會發現,除名字外,一個異常和下一個異常之間並不存在任何特殊的地方。此外,Java提供的異常數量正在日益增多;從本質上說,把它們印到一本書裡是沒有意義的。大家從其他地方獲得的任何新庫可能也提供了它們自己的異常。我們最需要掌握的是基本概念,以及用這些異常能夠做什麼。

java.lang.Exception

這是程序能捕獲的基本異常。其他異常都是從它派生出去的。這裡要注意的是異常的名字代表發生的問題,而且異常名通常都是精心挑選的,可以很清楚地說明到底發生了什麼事情。異常並不全是在java.lang中定義的;有些是為了提供對其他庫的支持,如utilnet以及io等——我們可以從它們的完整類名中看出這一點,或者觀察它們從什麼繼承。例如,所有IO異常都是從java.io.IOException繼承的。

9.3.1 RuntimeException的特殊情況

本章的第一個例子是:

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

看起來似乎在傳遞進入一個方法的每個引用中都必須檢查null(因為不知道調用者是否已傳遞了一個有效的引用),這無疑是相當可怕的。但幸運的是,我們根本不必這樣做——它屬於Java進行的標準運行期檢查的一部分。若對一個空引用發出了調用,Java會自動產生一個NullPointerException異常。所以上述代碼在任何情況下都是多餘的。

這個類別裡含有一系列異常類型。它們全部由Java自動生成,毋需我們親自動手把它們包含到自己的異常規範裡。最方便的是,通過將它們置入單獨一個名為RuntimeException的基類下面,它們全部組合到一起。這是一個很好的繼承例子:它建立了一系列具有某種共通性的類型,都具有某些共通的特徵與行為。此外,我們沒必要專門寫一個異常規範,指出一個方法可能會“拋”出一個RuntimeException,因為已經假定可能出現那種情況。由於它們用於指出編程中的錯誤,所以幾乎永遠不必專門捕獲一個“運行期異常”——RuntimeException——它在默認情況下會自動得到處理。若必須檢查RuntimeException,我們的代碼就會變得相當繁複。在我們自己的包裡,可選擇“拋”出一部分RuntimeException

如果不捕獲這些異常,又會出現什麼情況呢?由於編譯器並不強制異常規範捕獲它們,所以假如不捕獲的話,一個RuntimeException可能過濾掉我們到達main()方法的所有途徑。為體會此時發生的事情,請試試下面這個例子:

//: NeverCaught.java
// Ignoring RuntimeExceptions

public class NeverCaught {
  static void f() {
    throw new RuntimeException("From f()");
  }
  static void g() {
    f();
  }
  public static void main(String[] args) {
    g();
  }
} ///:~

大家已經看到,一個RuntimeException(或者從它繼承的任何東西)屬於一種特殊情況,因為編譯器不要求為這些類型指定異常規範。

輸出如下:

java.lang.RuntimeException: From f()
at NeverCaught.f(NeverCaught.java:9)
at NeverCaught.g(NeverCaught.java:12)
at NeverCaught.main(NeverCaught.java:15)

所以答案就是:假若一個RuntimeException獲得到達main()的所有途徑,同時不被捕獲,那麼當程序退出時,會為那個異常調用printStackTrace()

注意也許能在自己的代碼中僅忽略RuntimeException,因為編譯器已正確實行了其他所有控制。因為RuntimeException在此時代表一個編程錯誤:

(1) 一個我們不能捕獲的錯誤(例如,由客戶程序員接收傳遞給自己方法的一個空引用)。

(2) 作為一名程序員,一個應在自己的代碼中檢查的錯誤(如ArrayIndexOutOfBoundException,此時應注意數組的大小)。 可以看出,最好的做法是在這種情況下異常,因為它們有助於程序的調試。

另外一個有趣的地方是,我們不可將Java異常劃分為單一用途的工具。的確,它們設計用於控制那些討厭的運行期錯誤——由代碼控制範圍之外的其他力量產生。但是,它也特別有助於調試某些特殊類型的編程錯誤,那些是編譯器偵測不到的。

9.4 創建自己的異常

並不一定非要使用Java異常。這一點必須掌握,因為經常都需要創建自己的異常,以便指出自己的庫可能生成的一個特殊錯誤——但創建Java分級結構的時候,這個錯誤是無法預知的。

為創建自己的異常類,必須從一個現有的異常類型繼承——最好在含義上與新異常近似。繼承一個異常相當簡單:

//: Inheriting.java
// Inheriting your own exceptions

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

public class Inheriting {
  public static void f() throws MyException {
    System.out.println(
      "Throwing MyException from f()");
    throw new MyException();
  }
  public static void g() throws MyException {
    System.out.println(
      "Throwing MyException from g()");
    throw new MyException("Originated in g()");
  }
  public static void main(String[] args) {
    try {
      f();
    } catch(MyException e) {
      e.printStackTrace();
    }
    try {
      g();
    } catch(MyException e) {
      e.printStackTrace();
    }
  }
} ///:~

繼承在創建新類時發生:

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

這裡的關鍵是extends Exception,它的意思是:除包括一個Exception的全部含義以外,還有更多的含義。增加的代碼數量非常少——實際只添加了兩個構造器,對MyException的創建方式進行了定義。請記住,假如我們不明確調用一個基類構造器,編譯器會自動調用基類默認構造器。在第二個構造器中,通過使用super關鍵字,明確調用了帶有一個String參數的基類構造器。

該程序輸出結果如下:

Throwing MyException from f()
MyException
        at Inheriting.f(Inheriting.java:16)
        at Inheriting.main(Inheriting.java:24)
Throwing MyException from g()
MyException: Originated in g()
        at Inheriting.g(Inheriting.java:20)
        at Inheriting.main(Inheriting.java:29)

可以看到,在從f()“拋”出的MyException異常中,缺乏詳細的消息。

創建自己的異常時,還可以採取更多的操作。我們可添加額外的構造器及成員:

//: Inheriting2.java
// Inheriting your own exceptions

class MyException2 extends Exception {
  public MyException2() {}
  public MyException2(String msg) {
    super(msg);
  }
  public MyException2(String msg, int x) {
    super(msg);
    i = x;
  }
  public int val() { return i; }
  private int i;
}

public class Inheriting2 {
  public static void f() throws MyException2 {
    System.out.println(
      "Throwing MyException2 from f()");
    throw new MyException2();
  }
  public static void g() throws MyException2 {
    System.out.println(
      "Throwing MyException2 from g()");
    throw new MyException2("Originated in g()");
  }
  public static void h() throws MyException2 {
    System.out.println(
      "Throwing MyException2 from h()");
    throw new MyException2(
      "Originated in h()", 47);
  }
  public static void main(String[] args) {
    try {
      f();
    } catch(MyException2 e) {
      e.printStackTrace();
    }
    try {
      g();
    } catch(MyException2 e) {
      e.printStackTrace();
    }
    try {
      h();
    } catch(MyException2 e) {
      e.printStackTrace();
      System.out.println("e.val() = " + e.val());
    }
  }
} ///:~

此時添加了一個數據成員i;同時添加了一個特殊的方法,用它讀取那個值;也添加了一個額外的構造器,用它設置那個值。輸出結果如下:

Throwing MyException2 from f()
MyException2
        at Inheriting2.f(Inheriting2.java:22)
        at Inheriting2.main(Inheriting2.java:34)
Throwing MyException2 from g()
MyException2: Originated in g()
        at Inheriting2.g(Inheriting2.java:26)
        at Inheriting2.main(Inheriting2.java:39)
Throwing MyException2 from h()
MyException2: Originated in h()
        at Inheriting2.h(Inheriting2.java:30)
        at Inheriting2.main(Inheriting2.java:44)
e.val() = 47

由於異常不過是另一種形式的對象,所以可以繼續這個進程,進一步增強異常類的能力。但要注意,對使用自己這個包的客戶程序員來說,他們可能錯過所有這些增強。因為他們可能只是簡單地尋找準備生成的異常,除此以外不做任何事情——這是大多數Java庫異常的標準用法。若出現這種情況,有可能創建一個新異常類型,其中幾乎不包含任何代碼:

//: SimpleException.java
class SimpleException extends Exception {
} ///:~

它要依賴編譯器來創建默認構造器(會自動調用基類的默認構造器)。當然,在這種情況下,我們不會得到一個SimpleException(String)構造器,但它實際上也不會經常用到。

9.5 異常的限制

覆蓋一個方法時,只能產生已在方法的基類版本中定義的異常。這是一個重要的限制,因為它意味著與基類協同工作的代碼也會自動應用於從基類派生的任何對象(當然,這屬於基本的OOP概念),其中包括異常。

下面這個例子演示了強加在異常身上的限制類型(在編譯期):

//: StormyInning.java
// Overridden methods may throw only the
// exceptions specified in their base-class
// versions, or exceptions derived from the
// base-class exceptions.

class BaseballException extends Exception {}
class Foul extends BaseballException {}
class Strike extends BaseballException {}

abstract class Inning {
  Inning() throws BaseballException {}
  void event () throws BaseballException {
   // Doesn't actually have to throw anything
  }
  abstract void atBat() throws Strike, Foul;
  void walk() {} // Throws nothing
}

class StormException extends Exception {}
class RainedOut extends StormException {}
class PopFoul extends Foul {}

interface Storm {
  void event() throws RainedOut;
  void rainHard() throws RainedOut;
}

public class StormyInning extends Inning
    implements Storm {
  // OK to add new exceptions for constructors,
  // but you must deal with the base constructor
  // exceptions:
  StormyInning() throws RainedOut,
    BaseballException {}
  StormyInning(String s) throws Foul,
    BaseballException {}
  // Regular methods must conform to base class:
//! void walk() throws PopFoul {} //Compile error
  // Interface CANNOT add exceptions to existing
  // methods from the base class:
//! public void event() throws RainedOut {}
  // If the method doesn't already exist in the
  // base class, the exception is OK:
  public void rainHard() throws RainedOut {}
  // You can choose to not throw any exceptions,
  // even if base version does:
  public void event() {}
  // Overridden methods can throw
  // inherited exceptions:
  void atBat() throws PopFoul {}
  public static void main(String[] args) {
    try {
      StormyInning si = new StormyInning();
      si.atBat();
    } catch(PopFoul e) {
    } catch(RainedOut e) {
    } catch(BaseballException e) {}
    // Strike not thrown in derived version.
    try {
      // What happens if you upcast?
      Inning i = new StormyInning();
      i.atBat();
      // You must catch the exceptions from the
      // base-class version of the method:
    } catch(Strike e) {
    } catch(Foul e) {
    } catch(RainedOut e) {
    } catch(BaseballException e) {}
  }
} ///:~

Inning中,可以看到無論構造器還是event()方法都指出自己會“拋”出一個異常,但它們實際上沒有那樣做。這是合法的,因為它允許我們強迫用戶捕獲可能在覆蓋過的event()版本里添加的任何異常。同樣的道理也適用於abstract方法,就象在atBat()裡展示的那樣。

interface Storm非常有趣,因為它包含了在Incoming中定義的一個方法——event(),以及不是在其中定義的一個方法。這兩個方法都會“拋”出一個新的異常類型:RainedOut。當執行到StormyInning extendsimplements Storm的時候,可以看到Storm中的event()方法不能改變Inning中的event()的異常接口。同樣地,這種設計是十分合理的;否則的話,當我們操作基類時,便根本無法知道自己捕獲的是否正確的東西。當然,假如interface中定義的一個方法不在基類裡,比如rainHard(),它產生異常時就沒什麼問題。

對異常的限制並不適用於構造器。在StormyInning中,我們可看到一個構造器能夠“拋”出它希望的任何東西,無論基類構造器“拋”出什麼。然而,由於必須堅持按某種方式調用基類構造器(在這裡,會自動調用默認構造器),所以派生類構造器必須在自己的異常規範中聲明所有基類構造器異常。

StormyInning.walk()不會編譯的原因是它“拋”出了一個異常,而Inning.walk()卻不會“拋”出。若允許這種情況發生,就可讓自己的代碼調用Inning.walk(),而且它不必控制任何異常。但在以後替換從Inning派生的一個類的對象時,異常就會“拋”出,造成代碼執行的中斷。通過強迫派生類方法遵守基類方法的異常規範,對象的替換可保持連貫性。

覆蓋過的event()方法向我們顯示出一個方法的派生類版本可以不產生任何異常——即便基類版本要產生異常。同樣地,這樣做是必要的,因為它不會中斷那些已假定基類版本會產生異常的代碼。差不多的道理亦適用於atBat(),它會“拋”出PopFoul——從Foul派生出來的一個異常,而Foul異常是由atBat()的基類版本產生的。這樣一來,假如有人在自己的代碼裡操作Inning,同時調用了atBat(),就必須捕獲Foul異常。由於PopFoul是從Foul派生的,所以異常控制器(模塊)也會捕獲PopFoul

最後一個有趣的地方在main()內部。在這個地方,假如我們明確操作一個StormyInning對象,編譯器就會強迫我們只捕獲特定於那個類的異常。但假如我們向上轉換到基類型,編譯器就會強迫我們捕獲針對基類的異常。通過所有這些限制,異常控制代碼的“健壯”程度獲得了大幅度改善(註釋③)。

③:ANSI/ISO C++施加了類似的限制,要求派生方法異常與基類方法拋出的異常相同,或者從後者派生。在這種情況下,C++實際上能夠在編譯期間檢查異常規範。

我們必須認識到這一點:儘管異常規範是由編譯器在繼承期間強行遵守的,但異常規範並不屬於方法類型的一部分,後者僅包括了方法名以及參數類型。因此,我們不可在異常規範的基礎上覆蓋方法。除此以外,儘管異常規範存在於一個方法的基類版本中,但並不表示它必須在方法的派生類版本中存在。這與方法的“繼承”頗有不同(進行繼承時,基類中的方法也必須在派生類中存在)。換言之,用於一個特定方法的“異常規範接口”可能在繼承和覆蓋時變得更“窄”,但它不會變得更“寬”——這與繼承時的類接口規則是正好相反的。

9.6 用finally清除

無論一個異常是否在try塊中發生,我們經常都想執行一些特定的代碼。對一些特定的操作,經常都會遇到這種情況,但在恢復內存時一般都不需要(因為垃圾收集器會自動照料一切)。為達到這個目的,可在所有異常控制器的末尾使用一個finally從句(註釋④)。所以完整的異常控制小節象下面這個樣子:

try {
// 要保衛的區域:
// 可能“拋”出A,B,或C的危險情況
} catch (A a1) {
// 控制器 A
} catch (B b1) {
// 控制器 B
} catch (C c1) {
// 控制器 C
} finally {
// 每次都會發生的情況
}

④:C++異常控制未提供finally從句,因為它依賴構造器來達到這種清除效果。

為演示finally從句,請試驗下面這個程序:

//: FinallyWorks.java
// The finally clause is always executed

public class FinallyWorks {
  static int count = 0;
  public static void main(String[] args) {
    while(true) {
      try {
        // post-increment is zero first time:
        if(count++ == 0)
          throw new Exception();
        System.out.println("No exception");
      } catch(Exception e) {
        System.out.println("Exception thrown");
      } finally {
        System.out.println("in finally clause");
        if(count == 2) break; // out of "while"
      }
    }
  }
} ///:~

通過該程序,我們亦可知道如何應付Java異常(類似C++的異常)不允許我們恢復至異常產生地方的這一事實。若將自己的try塊置入一個循環內,就可建立一個條件,它必須在繼續程序之前滿足。亦可添加一個static計數器或者另一些設備,允許循環在放棄以前嘗試數種不同的方法。這樣一來,我們的程序可以變得更加“健壯”。

輸出如下:

Exception thrown
in finally clause
No exception
in finally clause

無論是否“拋”出一個異常,finally從句都會執行。

9.6.1 用finally做什麼

在沒有“垃圾收集”以及“自動調用析構器”機制的一種語言中(註釋⑤),finally顯得特別重要,因為程序員可用它擔保內存的正確釋放——無論在try塊內部發生了什麼狀況。但Java提供了垃圾收集機制,所以內存的釋放幾乎絕對不會成為問題。另外,它也沒有構造器可供調用。既然如此,Java裡何時才會用到finally呢?

⑤:“析構器”(Destructor)是“構造器”(Constructor)的反義詞。它代表一個特殊的函數,一旦某個對象失去用處,通常就會調用它。我們肯定知道在哪裡以及何時調用析構器。C++提供了自動的析構器調用機制,但Delphi的Object Pascal版本1及2卻不具備這一能力(在這種語言中,析構器的含義與用法都發生了變化)。

除將內存設回原始狀態以外,若要設置另一些東西,finally就是必需的。例如,我們有時需要打開一個文件或者建立一個網絡連接,或者在屏幕上畫一些東西,甚至設置外部世界的一個開關,等等。如下例所示:

//: OnOffSwitch.java
// Why use finally?

class Switch {
  boolean state = false;
  boolean read() { return state; }
  void on() { state = true; }
  void off() { state = false; }
}

public class OnOffSwitch {
  static Switch sw = new Switch();
  public static void main(String[] args) {
    try {
      sw.on();
      // Code that can throw exceptions...
      sw.off();
    } catch(NullPointerException e) {
      System.out.println("NullPointerException");
      sw.off();
    } catch(IllegalArgumentException e) {
      System.out.println("IOException");
      sw.off();
    }
  }
} ///:~

這裡的目標是保證main()完成時開關處於關閉狀態,所以將sw.off()置於try塊以及每個異常控制器的末尾。但產生的一個異常有可能不是在這裡捕獲的,這便會錯過sw.off()。然而,利用finally,我們可以將來自try塊的關閉代碼只置於一個地方:

//: WithFinally.java
// Finally Guarantees cleanup

class Switch2 {
  boolean state = false;
  boolean read() { return state; }
  void on() { state = true; }
  void off() { state = false; }
}

public class WithFinally {
  static Switch2 sw = new Switch2();
  public static void main(String[] args) {
    try {
      sw.on();
      // Code that can throw exceptions...
    } catch(NullPointerException e) {
      System.out.println("NullPointerException");
    } catch(IllegalArgumentException e) {
      System.out.println("IOException");
    } finally {
      sw.off();
    }
  }
} ///:~

在這兒,sw.off()已移至一個地方。無論發生什麼事情,都肯定會運行它。

即使異常不在當前的catch從句集裡捕獲,finally都會在異常控制機制轉到更高級別搜索一個控制器之前得以執行。如下所示:

//: AlwaysFinally.java
// Finally is always executed

class Ex extends Exception {}

public class AlwaysFinally {
  public static void main(String[] args) {
    System.out.println(
      "Entering first try block");
    try {
      System.out.println(
        "Entering second try block");
      try {
        throw new Ex();
      } finally {
        System.out.println(
          "finally in 2nd try block");
      }
    } catch(Ex e) {
      System.out.println(
        "Caught Ex in first try block");
    } finally {
      System.out.println(
        "finally in 1st try block");
    }
  }
} ///:~

該程序的輸出展示了具體發生的事情:

Entering first try block
Entering second try block
finally in 2nd try block
Caught Ex in first try block
finally in 1st try block

若調用了breakcontinue語句,finally語句也會得以執行。請注意,與作上標籤的breakcontinue一道,finally排除了Java對goto跳轉語句的需求。

9.6.2 缺點:丟失的異常

一般情況下,Java的異常實現方案都顯得十分出色。不幸的是,它依然存在一個缺點。儘管異常指出程序裡存在一個危機,而且絕不應忽略,但一個異常仍有可能簡單地“丟失”。在採用finally從句的一種特殊配置下,便有可能發生這種情況:

//: LostMessage.java
// How an exception can be lost

class VeryImportantException extends Exception {
  public String toString() {
    return "A very important exception!";
  }
}

class HoHumException extends Exception {
  public String toString() {
    return "A trivial exception";
  }
}

public class LostMessage {
  void f() throws VeryImportantException {
    throw new VeryImportantException();
  }
  void dispose() throws HoHumException {
    throw new HoHumException();
  }
  public static void main(String[] args)
      throws Exception {
    LostMessage lm = new LostMessage();
    try {
      lm.f();
    } finally {
      lm.dispose();
    }
  }
} ///:~

輸出如下:

A trivial exception
        at LostMessage.dispose(LostMessage.java:21)
        at LostMessage.main(LostMessage.java:29)

可以看到,這裡不存在VeryImportantException(非常重要的異常)的跡象,它只是簡單地被finally從句中的HoHumException代替了。

這是一項相當嚴重的缺陷,因為它意味著一個異常可能完全丟失。而且就象前例演示的那樣,這種丟失顯得非常“自然”,很難被人查出蛛絲馬跡。而與此相反,C++裡如果第二個異常在第一個異常得到控制前產生,就會被當作一個嚴重的編程錯誤處理。或許Java以後的版本會糾正這個問題(上述結果是用Java 1.1生成的)。

9.7 構造器

為異常編寫代碼時,我們經常要解決的一個問題是:“一旦產生異常,會正確地進行清除嗎?”大多數時候都會非常安全,但在構造器中卻是一個大問題。構造器將對象置於一個安全的起始狀態,但它可能執行一些操作——如打開一個文件。除非用戶完成對象的使用,並調用一個特殊的清除方法,否則那些操作不會得到正確的清除。若從一個構造器內部“拋”出一個異常,這些清除行為也可能不會正確地發生。所有這些都意味著在編寫構造器時,我們必須特別加以留意。

由於前面剛學了finally,所以大家可能認為它是一種合適的方案。但事情並沒有這麼簡單,因為finally每次都會執行清除代碼——即使我們在清除方法運行之前不想執行清除代碼。因此,假如真的用finally進行清除,必須在構造器正常結束時設置某種形式的標誌。而且只要設置了標誌,就不要執行finally塊內的任何東西。由於這種做法並不完美(需要將一個地方的代碼同另一個地方的結合起來),所以除非特別需要,否則一般不要嘗試在finally中進行這種形式的清除。

在下面這個例子裡,我們創建了一個名為InputFile的類。它的作用是打開一個文件,然後每次讀取它的一行內容(轉換為一個字符串)。它利用了由Java標準IO庫提供的FileReader以及BufferedReader類(將於第10章討論)。這兩個類都非常簡單,大家現在可以毫無困難地掌握它們的基本用法:

//: Cleanup.java
// Paying attention to exceptions
// in constructors
import java.io.*;

class InputFile {
  private BufferedReader in;
  InputFile(String fname) throws Exception {
    try {
      in =
        new BufferedReader(
          new FileReader(fname));
      // Other code that might throw exceptions
    } catch(FileNotFoundException e) {
      System.out.println(
        "Could not open " + fname);
      // Wasn't open, so don't close it
      throw e;
    } catch(Exception e) {
      // All other exceptions must close it
      try {
        in.close();
      } catch(IOException e2) {
        System.out.println(
          "in.close() unsuccessful");
      }
      throw e;
    } finally {
      // Don't close it here!!!
    }
  }
  String getLine() {
    String s;
    try {
      s = in.readLine();
    } catch(IOException e) {
      System.out.println(
        "readLine() unsuccessful");
      s = "failed";
    }
    return s;
  }
  void cleanup() {
    try {
      in.close();
    } catch(IOException e2) {
      System.out.println(
        "in.close() unsuccessful");
    }
  }
}

public class Cleanup {
  public static void main(String[] args) {
    try {
      InputFile in =
        new InputFile("Cleanup.java");
      String s;
      int i = 1;
      while((s = in.getLine()) != null)
        System.out.println(""+ i++ + ": " + s);
      in.cleanup();
    } catch(Exception e) {
      System.out.println(
        "Caught in main, e.printStackTrace()");
      e.printStackTrace();
    }
  }
} ///:~

該例使用了Java 1.1 IO類。

用於InputFile的構造器採用了一個String(字符串)參數,它代表我們想打開的那個文件的名字。在一個try塊內部,它用該文件名創建了一個FileReader。對FileReader來說,除非轉移並用它創建一個能夠實際與之“交談”的BufferedReader,否則便沒什麼用處。注意InputFile的一個好處就是它同時合併了這兩種行動。

FileReader構造器不成功,就會產生一個FileNotFoundException(文件未找到異常)。必須單獨捕獲這個異常——這屬於我們不想關閉文件的一種特殊情況,因為文件尚未成功打開。其他任何捕獲從句(catch)都必須關閉文件,因為文件已在進入那些捕獲從句時打開(當然,如果多個方法都能產生一個FileNotFoundException異常,就需要稍微用一些技巧。此時,我們可將不同的情況分隔到數個try塊內)。close()方法會拋出一個嘗試過的異常。即使它在另一個catch從句的代碼塊內,該異常也會得以捕獲——對Java編譯器來說,那個catch從句不過是另一對花括號而已。執行完本地操作後,異常會被重新“拋”出。這樣做是必要的,因為這個構造器的執行已經失敗,我們不希望調用方法來假設對象已正確創建以及有效。

在這個例子中,沒有采用前述的標誌技術,finally從句顯然不是關閉文件的正確地方,因為這可能在每次構造器結束的時候關閉它。由於我們希望文件在InputFile對象處於活動狀態時一直保持打開狀態,所以這樣做並不恰當。

getLine()方法會返回一個字符串,其中包含了文件中下一行的內容。它調用了readLine(),後者可能產生一個異常,但那個異常會被捕獲,使getLine()不會再產生任何異常。對異常來說,一項特別的設計問題是決定在這一級完全控制一個異常,還是進行部分控制,並傳遞相同(或不同)的異常,或者只是簡單地傳遞它。在適當的時候,簡單地傳遞可極大簡化我們的編碼工作。

getLine()方法會變成:

String getLine() throws IOException {
return in.readLine();
}

但是當然,調用者現在需要對可能產生的任何IOException進行控制。

用戶使用完畢InputFile對象後,必須調用cleanup()方法,以便釋放由BufferedReader以及/或者FileReader佔用的系統資源(如文件引用)——註釋⑥。除非InputFile對象使用完畢,而且到了需要棄之不用的時候,否則不應進行清除。大家可能想把這樣的機制置入一個finalize()方法內,但正如第4章指出的那樣,並非總能保證finalize()獲得正確的調用(即便確定它會調用,也不知道何時開始)。這屬於Java的一項缺陷——除內存清除之外的所有清除都不會自動進行,所以必須知會客戶程序員,告訴他們有責任用finalize()保證清除工作的正確進行。

⑥:在C++裡,“析構器”可幫我們控制這一局面。

Cleanup.java中,我們創建了一個InputFile,用它打開用於創建程序的相同的源文件。同時一次讀取該文件的一行內容,而且添加相應的行號。所有異常都會在main()中被捕獲——儘管我們可選擇更大的可靠性。

這個示例也向大家展示了為何在本書的這個地方引入異常的概念。異常與Java的編程具有很高的集成度,這主要是由於編譯器會強制它們。只有知道了如何操作那些異常,才可更進一步地掌握編譯器的知識。

9.8 異常匹配

“拋”出一個異常後,異常控制系統會按當初編寫的順序搜索“最接近”的控制器。一旦找到相符的控制器,就認為異常已得到控制,不再進行更多的搜索工作。

在異常和它的控制器之間,並不需要非常精確的匹配。一個派生類對象可與基類的一個控制器相配,如下例所示:

//: Human.java
// Catching Exception Hierarchies

class Annoyance extends Exception {}
class Sneeze extends Annoyance {}

public class Human {
  public static void main(String[] args) {
    try {
      throw new Sneeze();
    } catch(Sneeze s) {
      System.out.println("Caught Sneeze");
    } catch(Annoyance a) {
      System.out.println("Caught Annoyance");
    }
  }
} ///:~

Sneeze異常會被相符的第一個catch從句捕獲。當然,這只是第一個。然而,假如我們刪除第一個catch從句:

    try {
      throw new Sneeze();
    } catch(Annoyance a) {
      System.out.println("Caught Annoyance");
    }

那麼剩下的catch從句依然能夠工作,因為它捕獲的是Sneeze的基類。換言之,catch(Annoyance e)能捕獲一個Annoyance以及從它派生的任何類。這一點非常重要,因為一旦我們決定為一個方法添加更多的異常,而且它們都是從相同的基類繼承的,那麼客戶程序員的代碼就不需要更改。至少能夠假定它們捕獲的是基類。

若將基類捕獲從句置於第一位,試圖“屏蔽”派生類異常,就象下面這樣:

    try {
      throw new Sneeze();
    } catch(Annoyance a) {
      System.out.println("Caught Annoyance");
    } catch(Sneeze s) {
      System.out.println("Caught Sneeze");
    }

則編譯器會產生一條出錯消息,因為它發現永遠不可能抵達Sneeze捕獲從句。

9.8.1 異常準則

用異常做下面這些事情:

(1) 解決問題並再次調用造成異常的方法。

(2) 平息事態的發展,並在不重新嘗試方法的前提下繼續。

(3) 計算另一些結果,而不是希望方法產生的結果。

(4) 在當前環境中儘可能解決問題,以及將相同的異常重新“拋”出一個更高級的環境。

(5) 在當前環境中儘可能解決問題,以及將不同的異常重新“拋”出一個更高級的環境。

(6) 中止程序執行。

(7) 簡化編碼。若異常方案使事情變得更加複雜,那就會令人非常煩惱,不如不用。

(8) 使自己的庫和程序變得更加安全。這既是一種“短期投資”(便於調試),也是一種“長期投資”(改善應用程序的健壯性)

9.9 總結

通過先進的錯誤糾正與恢復機制,我們可以有效地增強代碼的健壯程度。對我們編寫的每個程序來說,錯誤恢復都屬於一個基本的考慮目標。它在Java中顯得尤為重要,因為該語言的一個目標就是創建不同的程序組件,以便其他用戶(客戶程序員)使用。為構建一套健壯的系統,每個組件都必須非常健壯。

在Java裡,異常控制的目的是使用盡可能精簡的代碼創建大型、可靠的應用程序,同時排除程序裡那些不能控制的錯誤。

異常的概念很難掌握。但只有很好地運用它,才可使自己的項目立即獲得顯著的收益。Java強迫遵守異常所有方面的問題,所以無論庫設計者還是客戶程序員,都能夠連續一致地使用它。

9.10 練習

(1) 用main()創建一個類,令其拋出try塊內的Exception類的一個對象。為Exception的構造器賦予一個字符串參數。在catch從句內捕獲異常,並打印出字符串參數。添加一個finally從句,並打印一條消息,證明自己真正到達那裡。

(2) 用extends關鍵字創建自己的異常類。為這個類寫一個構造器,令其採用String參數,並隨同String引用把它保存到對象內。寫一個方法,令其打印出保存下來的String。創建一個try-catch從句,練習實際操作新異常。

(3) 寫一個類,並令一個方法拋出在練習2中創建的類型的一個異常。試著在沒有異常規範的前提下編譯它,觀察編譯器會報告什麼。接著添加適當的異常規範。在一個try-catch從句中嘗試自己的類以及它的異常。

(4) 在第5章,找到調用了Assert.java的兩個程序,並修改它們,令其拋出自己的異常類型,而不是打印到System.err。該異常應是擴展了RuntimeException的一個內部類。