Thinking in java 第12章 通過異常處理錯誤
12.1 概念
1. 使用一場所帶來的一個好處是,它忘完更夠降低錯誤處理代碼的複雜度。如果不使用異常,那麼就必須檢查特定的的錯誤,並在程序中的許多地方去處理它。而如果使用異常,就不必在方法調用處進行檢查,並且只需在一個地方處理錯誤,即異常處理程序中。這使得代碼的閱讀、編寫和調試工作更加井井有條。
12.2 基本異常
1. 同Java其他對象的創建一樣,將使用new在堆上創建異常對象。
2. 異常最重要的方面之一就是如果發生問題,他們將不允許程序沿着其正常的路徑繼續走下去。
3. 所有標準異常類都有兩個構造器:一個是默認構造器,一個是接受字符串作爲參數,以便能把相關信息放入異常對象的構造器。
throw new NullPointerException("t = null");
4. 異常返回的“地點”和普通方法調用返回的“地點”不同,可能離得很遠,也可能跨越方法調用棧很多層次。
5. 能夠拋出任意類型的Throwable對象是異常類型的根類。
12.3 捕獲異常
1. 格式如下:
try{
//Code that might generate exceptions
} catch (Type1 id1) {
// Handle exceptions of Type1
} catch (Type2 id2) {
// ...
} ...
2. 在try塊內部,許多不同的方法調用可能會產生類型相同的異常,而你只需要提供一個針對此類型的異常處理程序。
3. 異常處理理論上有兩種基本模型:中止模型和恢復模型。Java支持中止模型,如果想要Java實現類似恢復的行爲,那麼在遇到錯誤時就不能拋出異常,而是調用方法來修正。或者把try塊放在while裏,直到得到滿意的結果。
12.4 創建自定義異常
1. 要自己定義異常類,必須從已有的異常類繼承,最好是選擇意思詳盡的異常類繼承(不過並不容易找)。
class SimpleException extends Exception {
SimpleException() {}
SimpleException(String msg) { super(msg); }
}
public class A {
public void f() throws SimpleException {
print("...");
throw new SimpleException();
}
public static void main(String[] args) {
try {
new A().f();
} catch(SimpleException e) {
print("Catch!");
}
}
}
2. printStackTrace()方法是在Throwable中的(Exception繼承它),它將打印“從方法調用處直到異常跑出處”的方法調用序列。可加上參數“System.out”直接顯示在輸出中,默認版本則被輸出到標準錯誤流(System.err)。
3. 異常與記錄日誌(P255例,看完18章再回來看)。
12.5 異常說明
1. 異常說明屬於方法聲明的一部分,緊跟在形式參數列表之後。
2. 可以不止一個。也可以聲明瞭之後不拋出,但拋出後要麼聲明要麼處理。
12.6 捕獲所有異常
1. 舉例說明了一些方法,如:printStackTrace()、getStackTrace()(用於配合後者進行遍歷棧軌跡)、StackTraceElement、fillInStackTrace()(更新異常發生地)。
2. 異常鏈:在捕獲一個異常後拋出另一個異常,並希望把原始異常信息保存下來。
3. 現在所有Throwable的子類在構造其中都可以機收一個cause對象作爲參數,而只有Error(用於Java虛擬機報告系統錯誤)、Exception、RuntimeException提供了帶cause參數的構造器,其他類型要用initCause()方法而不是構造器。
12.8 使用finally進行清理
1. 對於一些代碼,可能會希望無論try塊中的異常是否拋出,它們都能得到執行。這通常適用於內存回收之外的情況。
try{
//Code that might generate exceptions
} catch (Type1 id1) {
// Handle exceptions of Type1
} catch (Type2 id2) {
// ...
} finally {
// ...
}
2. 用途:把除內存之外的資源恢復到它們的初始狀態,包括:已經打開的文件或網絡鏈接,在屏幕上畫的圖形,甚至可以使外部世界的某個開關。
3. 在異常沒有被當前的異常處理程序捕獲的情況下,異常處理機制會在跳到更高一層的異常處理程序之前執行finally字句。即如果有嵌套try塊,在進入裏面的try塊之前,外面的finally子句會被執行。
4. 在finally類內部,從何處返回無關緊要,一定會被執行。
5. Java中異常也容易丟失,比如被try塊中的異常被finally中的異常覆蓋。
12.11 異常匹配
1. 派生類的對象也可以匹配其基類的處理程序,且只會被處理一次。
2. 如果把捕獲的基類放在派生類之前,編譯器會報錯。
12.12 其他可選方式
1. 異常處理的一個重要原則是:只有在你知道如何處理的情況下才捕獲異常。
2. 編譯器會在你還沒準備好的時候就強制讓你加上catch字句(即暫時放置,後面可能就忘了),可能會導致異常被吞食。
12.13 異常使用指南
1. 應該在下列情況下使用異常:
- 在恰當的級別處理問題。(在知道該如何處理的情況下才捕獲異常)
- 解決問題並且重新調用產生異常的方法。
- 進行少許修補,然後繞過異常發生的地方繼續執行。
- 用別的數據進行計算,以代替方法預計會返回的值。
- 把當前運行環境下能做的事情儘量做完,然後把相同的異常重拋到更高層。
- 把當前運行環境下能做的事儘量做完,然後把不同的異常拋到更高層。
- 終止程序。
- 進行簡化。(如果你的異常模式使問題變得太複雜,那用起來會非常痛苦也很煩人)
- 讓類庫和程序更安全。(這既是在爲調試做短期投資,也是在爲程序的健壯性做長期投資)
習題:
練習1:編寫一個類,在其main()方法的try塊裏拋出一個Exception類的對象。穿第一個字符串參數給Exception的構造器。在catch子句裏捕獲此異常對象,並且打印字符串參數。添加一個finally子句,打印一條信息以證明這裏確實得到了執行。
package Chapter12;
public class E1 {
public static void main(String[] args) {
try {
throw new E1A("aaa");
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
System.out.println("this is finally");
}
}
}
class E1A extends Exception {
public E1A() {}
public E1A(String msg) { super(msg); }
}
/*
aaa
this is finally
*/
練習2:定義一個對象引用並初始化爲null,嘗試用此引用調用方法。把這個引用放在try-catch子句裏以捕獲異常。
package Chapter12;
public class E2 {
public static void main(String[] args) {
String s = null;
//s.length();
try {
System.out.println(s.length());
} catch (NullPointerException e) {
System.out.println("this is a null pointer!");
}
}
}
/*
this is a null pointer!
*/
直接用會拋出NullPointerException異常。
練習3:編寫能產生並能捕獲ArrayIndexOutOfBoundsException異常的代碼。
package Chapter12;
public class E3 {
public static void main(String[] args) {
int[] a = new int[] {1,2,3,4,5};
try {
System.out.println(a[10]);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Warning: out of bounds!");
}
}
}
/*
Warning: out of bounds!
*/
練習4:使用extends關鍵宇建立-個自定義異常類。爲這個類寫一個接受字符串參數的構造器,把此參數保存在對象內部的字符串引用中。寫一個方法顯示此字符串。寫- -個try-catch子句,對這個新異常進行測試。
略。同練習1。
練習5:使用while循環建立類似恢復模型的異常處理行爲,它將不斷重複,直到異常不再拋出。
package Chapter12;
public class E4 {
public static void main(String[] args) {
int[] a = new int[] {1,2,3,4,5};
int index = 10;
while(true) {
try {
System.out.println(a[index]);
break;
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("" + index + " is out of bounds!");
index--;
}
}
}
}
/*
10 is out of bounds!
9 is out of bounds!
8 is out of bounds!
7 is out of bounds!
6 is out of bounds!
5 is out of bounds!
5
*/
練習6-練習7 日誌記錄。 略。
練習8:定義一個類,令其方法拋出在練習2裏定義的異常。不用異常說明,看看能否通過編譯。然後加上異常說明,用try-catch子句測試該類和異常。
package Chapter12;
public class E8 {
public static void main(String[] args) {
try {
new E8A().func1();
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("this is a ArrayIndexOutOfBoundsException");
}
}
}
class E8A {
public void func1() throws ArrayIndexOutOfBoundsException {
throw new ArrayIndexOutOfBoundsException();
}
}
/*
this is a ArrayIndexOutOfBoundsException
*/
因爲ArrayIndexOutOfBoundsException是RuntimeException的子類所以不聲明也可以,自定義的要聲明。
練習9:定義三種新的異常類型。寫一個類,在一個方法中拋出這三種異常。在main()方法裏調用這個方法,僅用一個catch子句捕獲這三種異常。
。。。?
package Chapter12;
public class E9 {
public static void main(String[] args) {
try {
func();
} catch (E9C e) {
e.printStackTrace(System.out);
}
}
public static void func() throws E9C {
try {
throw new E9A();
} catch (E9A e) {
e.printStackTrace(System.out);
}
try {
throw new E9B();
} catch (E9B e) {
e.printStackTrace(System.out);
throw new E9C();
}
}
}
class E9A extends Exception {}
class E9B extends Exception {}
class E9C extends Exception {}
/*
Chapter12.E9A
at Chapter12.E9.func(E9.java:13)
at Chapter12.E9.main(E9.java:6)
Chapter12.E9B
at Chapter12.E9.func(E9.java:19)
at Chapter12.E9.main(E9.java:6)
Chapter12.E9C
at Chapter12.E9.func(E9.java:22)
at Chapter12.E9.main(E9.java:6)
*/
練習10:爲一個類定義兩個方法:f() 和 g() 。在g()裏,拋出一個自定義的新異常。在f()裏調用g,捕獲它拋出的異常,並且在catch子句裏拋出另一個異常(自定義的第二種異常),在main()方法裏測試代碼。
略。同上。
練習11:重複上一個練習,但是在catch子句裏把g()要突出的異常包裝成一個RuntimeException。
意義不明。略。
練習12:修改innerclass/Sequence.java,使其在你試圖向其中放置過多地元素時拋出一個合適的異常。
略。if語句throw一個自定義異常。
練習13:修改練習9,加一個finally子句。驗證一下,即便是拋出NullPointerException異常,finally子句也會得到執行。
。。。?
練習14:試說明,在OnOffSwitch.java的try塊內拋出RuntimeException,程序可能會出錯誤。
因爲要被catch的異常沒有RuntimeException或者其基類。
練習15:試說明,在WithFinally.java的try塊內拋出RuntimeException,程序不會出錯誤。
?爲什麼。
練習16:修改reusing/CADSystem.java,以演示try-finally的中間返回仍會執行正確的清理。
略。知道finally肯定會執行就行了。
練習17:修改polymorphism/Frog.java,使其使用try-finally來保證正確的清理,並展示即使在try-finally的中間返回,它也可以起作用。
同上。
練習18:爲LostMessage.java添加第二層異常丟失,以便用第三個異常來替代HoHumException異常。
package Chapter12;
public class E18 {
public static void main(String[] args) {
try {
try {
f();
} finally {
try {
g();
} finally {
dispose();
}
}
} catch (Exception e) {
System.out.println(e);
}
}
public static void f() throws E18A {
throw new E18A();
}
public static void g() throws E18B {
throw new E18B();
}
public static void dispose() throws E18C {
throw new E18C();
}
}
class E18A extends Exception {
public String toString() {
return "E18A";
}
}
class E18B extends Exception {
public String toString() {
return "E18B";
}
}
class E18C extends Exception {
public String toString() {
return "E18C";
}
}
/*
E18C
*/
練習19:通過確保finally子句的調用,來修復LostMessage.java中的問題。
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 LostMessageFound19 {
void f() throws VeryImportantException {
throw new VeryImportantException();
}
void dispose() throws HoHumException {
throw new HoHumException();
}
public static void main(String[] args) {
try {
LostMessageFound19 lmf = new LostMessageFound19();
try {
lmf.f();
} catch(Exception e) {
System.out.println(e);
} finally {
lmf.dispose();
}
} catch(Exception e) {
System.out.println(e);
}
}
}
練習20:修改StormyInning.java,加一個UmpireArgument異常,和一個能拋出此異常的方法。測試一下修改後的異常繼承體系。
略。
練習21:試證明,派生類的構造器不能捕獲它的基類構造器所拋出的異常。
package Chapter12;
public class E21 {
}
class E21A extends Exception {}
class E21B {
E21B() throws E21A {
throw new E21A();
}
}
class E21C extends E21B {
E21C() throws E21A {
//super();
try {
super();
} catch (E21A e) {
System.out.println("catch!");
}
}
}
報錯,提示'super()' must be first statement in constructor body。
練習22:創建一個名爲FailingConstructor.java的類,它具有一個可能會在構造過程中失敗並且會拋出一個異常的構造器。在main()中,編寫能夠確保不出現故障的代碼,並在main()中驗證所有可能的故障情形都被覆蓋了。
package Chapter12;
public class E22 {
public static void main(String[] args) {
try {
E22FailingConstructor e = new E22FailingConstructor("aaa");
} catch (Exception e) {
System.out.println("Failed");
} finally {
System.out.println("End");
}
}
}
class E22FailingConstructor {
String s;
public E22FailingConstructor(String s) throws Exception {
this.s = s;
}
}
練習23:在前一個練習中添加dispose()方法。修改FailingConstructor,使其構造器可以將那些可去除對象之一當做一個成員對象創建,然後改構造器可能會拋出一個異常,之後他將創建第二個可去除成員對象。編寫能夠確保不出故障的代碼,並在main()中驗證所有可能的故障情形都被覆蓋了。
package Chapter12;
public class E23 {
public static void main(String[] args) {
try {
E23FailingConstructor e = new E23FailingConstructor("aaa");
} catch (Exception e) {
System.out.println("e failed");
}finally {
System.out.println("End");
}
}
}
class E23Dispose {
private boolean isDispose = false;
public boolean isDispose() {
return isDispose;
}
public void dispose() { isDispose = true; }
}
class E23FailingConstructor {
String s;
public E23FailingConstructor(String s) throws Exception {
try {
E23Dispose d1 = new E23Dispose();
try {
E23Dispose d2 = new E23Dispose();
} catch (Exception e) {
System.out.println("d2 failed");
d1.dispose();
}
} catch (Exception e) {
System.out.println("d1 failed");
} finally {
this.s = s;
}
}
}
練習24:在FailingConstructor類中添加一個dispose()方法,並編寫代碼正確使用這個類。
略。
練習25:建立一個三層的異常繼承體系,然後創建基類A,它的一個方法能拋出異常體系的基類異常,讓B繼承A,並且覆蓋這個方法,讓它拋出第二層的異常。讓C繼承B,再次覆蓋這個方法,讓它拋出第三層的異常。在main()裏創建一個C類型的對象,把它向上轉型爲A,然後調用這個方法。
略。同繼承。
練習26:沒有練習26?
練習27-練習30。略。