# 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`從句——即異常控制器——都類似一個小型方法,它需要采用一個(而且只有一個)特定類型的參數。可在控制器內部使用標識符(`id1`,`id2`等等),就象一個普通的方法參數那樣。我們有時也根本不使用標識符,因為異常類型已提供了足夠的信息,可有效處理異常。但即使不用,標識符也必須就位。
控制器必須“緊接”在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)
```
打印出`Throwable`和`Throwable`的調用棧路徑。調用棧顯示出將我們帶到異常發生地點的方法調用的順序。
第一個版本會打印出標準錯誤,第二個則打印出我們的選擇流程。若在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`對象的引用。由于`Throwable`是`Exception`的一個基類,所以有可能獲得一個能夠“拋”出的對象(具有`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`創建的、以內存堆為基礎的對象,所以垃圾收集器會自動將其清除。
- Java 編程思想
- 寫在前面的話
- 引言
- 第1章 對象入門
- 1.1 抽象的進步
- 1.2 對象的接口
- 1.3 實現方案的隱藏
- 1.4 方案的重復使用
- 1.5 繼承:重新使用接口
- 1.6 多態對象的互換使用
- 1.7 對象的創建和存在時間
- 1.8 異常控制:解決錯誤
- 1.9 多線程
- 1.10 永久性
- 1.11 Java和因特網
- 1.12 分析和設計
- 1.13 Java還是C++
- 第2章 一切都是對象
- 2.1 用引用操縱對象
- 2.2 所有對象都必須創建
- 2.3 絕對不要清除對象
- 2.4 新建數據類型:類
- 2.5 方法、參數和返回值
- 2.6 構建Java程序
- 2.7 我們的第一個Java程序
- 2.8 注釋和嵌入文檔
- 2.9 編碼樣式
- 2.10 總結
- 2.11 練習
- 第3章 控制程序流程
- 3.1 使用Java運算符
- 3.2 執行控制
- 3.3 總結
- 3.4 練習
- 第4章 初始化和清除
- 4.1 用構造器自動初始化
- 4.2 方法重載
- 4.3 清除:收尾和垃圾收集
- 4.4 成員初始化
- 4.5 數組初始化
- 4.6 總結
- 4.7 練習
- 第5章 隱藏實現過程
- 5.1 包:庫單元
- 5.2 Java訪問指示符
- 5.3 接口與實現
- 5.4 類訪問
- 5.5 總結
- 5.6 練習
- 第6章 類復用
- 6.1 組合的語法
- 6.2 繼承的語法
- 6.3 組合與繼承的結合
- 6.4 到底選擇組合還是繼承
- 6.5 protected
- 6.6 累積開發
- 6.7 向上轉換
- 6.8 final關鍵字
- 6.9 初始化和類裝載
- 6.10 總結
- 6.11 練習
- 第7章 多態性
- 7.1 向上轉換
- 7.2 深入理解
- 7.3 覆蓋與重載
- 7.4 抽象類和方法
- 7.5 接口
- 7.6 內部類
- 7.7 構造器和多態性
- 7.8 通過繼承進行設計
- 7.9 總結
- 7.10 練習
- 第8章 對象的容納
- 8.1 數組
- 8.2 集合
- 8.3 枚舉器(迭代器)
- 8.4 集合的類型
- 8.5 排序
- 8.6 通用集合庫
- 8.7 新集合
- 8.8 總結
- 8.9 練習
- 第9章 異常差錯控制
- 9.1 基本異常
- 9.2 異常的捕獲
- 9.3 標準Java異常
- 9.4 創建自己的異常
- 9.5 異常的限制
- 9.6 用finally清除
- 9.7 構造器
- 9.8 異常匹配
- 9.9 總結
- 9.10 練習
- 第10章 Java IO系統
- 10.1 輸入和輸出
- 10.2 增添屬性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File類
- 10.5 IO流的典型應用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 壓縮
- 10.9 對象序列化
- 10.10 總結
- 10.11 練習
- 第11章 運行期類型識別
- 11.1 對RTTI的需要
- 11.2 RTTI語法
- 11.3 反射:運行期類信息
- 11.4 總結
- 11.5 練習
- 第12章 傳遞和返回對象
- 12.1 傳遞引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只讀類
- 12.5 總結
- 12.6 練習
- 第13章 創建窗口和程序片
- 13.1 為何要用AWT?
- 13.2 基本程序片
- 13.3 制作按鈕
- 13.4 捕獲事件
- 13.5 文本字段
- 13.6 文本區域
- 13.7 標簽
- 13.8 復選框
- 13.9 單選鈕
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 視窗化應用
- 13.16 新型AWT
- 13.17 Java 1.1用戶接口API
- 13.18 可視編程和Beans
- 13.19 Swing入門
- 13.20 總結
- 13.21 練習
- 第14章 多線程
- 14.1 反應靈敏的用戶界面
- 14.2 共享有限的資源
- 14.3 堵塞
- 14.4 優先級
- 14.5 回顧runnable
- 14.6 總結
- 14.7 練習
- 第15章 網絡編程
- 15.1 機器的標識
- 15.2 套接字
- 15.3 服務多個客戶
- 15.4 數據報
- 15.5 一個Web應用
- 15.6 Java與CGI的溝通
- 15.7 用JDBC連接數據庫
- 15.8 遠程方法
- 15.9 總結
- 15.10 練習
- 第16章 設計模式
- 16.1 模式的概念
- 16.2 觀察器模式
- 16.3 模擬垃圾回收站
- 16.4 改進設計
- 16.5 抽象的應用
- 16.6 多重分發
- 16.7 訪問器模式
- 16.8 RTTI真的有害嗎
- 16.9 總結
- 16.10 練習
- 第17章 項目
- 17.1 文字處理
- 17.2 方法查找工具
- 17.3 復雜性理論
- 17.4 總結
- 17.5 練習
- 附錄A 使用非JAVA代碼
- 附錄B 對比C++和Java
- 附錄C Java編程規則
- 附錄D 性能
- 附錄E 關于垃圾收集的一些話
- 附錄F 推薦讀物