《java程式設計思想》 第十二章異常處理錯誤
12.4
之前程式裡寫日誌不清楚怎麼把printStackTrace()輸出的內容寫到日誌裡,僅僅是寫getMessage()資訊少了不少。在本節的例子中給出了一個方法:
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStrackTrace(pw);
logger.error(sw.toString());
12.5
可以宣告方法將異常丟擲,但實際上該方法並不丟擲異常。這樣做的好處是為異常先佔一個位子,以後可以丟擲這種異常而不用修改方法宣告。
在編譯時被強制檢查的異常稱為被檢查的異常。
練習8題中說“丟擲練習3裡定義的異常”錯了,應該是練習4中定義的異常。中有個細節需要注意:宣告方法時標識了丟擲的異常,即使方法內部並沒有真正丟擲此異常,呼叫該方法的時還是要處理異常,否則編譯器會報錯。
12.6
Exception是與程式設計有關的所有異常類的基類。它從Throwable類中繼承了一些方法:
String getMessage()
String getLocalizedMessage()
獲取異常資訊和用本地語言表示的異常資訊。
void printStackTrace()
void printStackTrace(PrintStream)
void printStackTrace(PrintWriter)
列印資訊和呼叫棧軌跡。第一個版本輸出到標準錯誤流,後兩個版本可以選擇要輸出的流。
Throwable fillInStrackTrace()這個方法的作用是對呼叫的物件重新填充呼叫棧,使得呼叫棧看起來和新建立的異常是相同的。經過編碼測試,方法返回的Throwable的物件和呼叫物件是同一個物件,看來只是改變了呼叫棧的資料,以下是測試程式碼:
public class FilInStackTraceTest {
public void f() throws Exception {
throw new Exception();
}
public void g() {
try {
f();
} catch (Exception e) {
e.printStackTrace();
Exception e1 = (Exception)e.fillInStackTrace();
System.out.println(e1 == e);
e.printStackTrace();
e1.printStackTrace();
}
}
public static void main(String[] args) {
FilInStackTraceTest test = new FilInStackTraceTest();
test.g();
}
}
輸出結果如下:
java.lang.Exception
at com.sfauto.exception.FilInStackTraceTest.f(FilInStackTraceTest.java:6)
at com.sfauto.exception.FilInStackTraceTest.g(FilInStackTraceTest.java:11)
at com.sfauto.exception.FilInStackTraceTest.main(FilInStackTraceTest.java:23)
java.lang.Exception
at com.sfauto.exception.FilInStackTraceTest.g(FilInStackTraceTest.java:14)
at com.sfauto.exception.FilInStackTraceTest.main(FilInStackTraceTest.java:23)
java.lang.Exception
at com.sfauto.exception.FilInStackTraceTest.g(FilInStackTraceTest.java:14)
at com.sfauto.exception.FilInStackTraceTest.main(FilInStackTraceTest.java:23)
true
從最後一行可以看出,e與e1是一個物件,呼叫fillInStackTrace()方法之前和之後e.printStackTrace()的列印結果不同,而呼叫filllInStackTrace()方法之後e與e1printStackTrace()的列印同。由此可見fillInStackTrace()是改變了呼叫物件的呼叫棧。
printStackTrace()方法所提供的資訊可以通過getStackTrace()方法來獲取,此方法將返回一個數組,每一個元素都表示棧中的一幀。元素0是棧頂元素,是呼叫序列中的最後一個方法呼叫(即Throwable被建立和丟擲之處,離異常最近的方法),陣列中最後一個元素是棧底元素,即呼叫的最外層方法。
常常會想要在捕獲一個異常後丟擲另一個異常,並且希望把原始異常的資訊儲存下來,這被稱為異常鏈。Throwable和它的一些子類提供了帶有引數Throwable cause的建構函式來維持異常鏈。書中說只有Error、Exception和RuntimeException提供這個建構函式,好像說的不對,至少我知道的SQLException和IOException都有這種建構函式。在沒有此種建構函式的情況下可以呼叫initCause()方法來達到相同的效果。
12.7
Throwable這個Java類被用來表示任何可以作為異常被丟擲的類。Throwable物件可以分為兩種型別:Error用來表示編譯時錯誤和系統錯誤,除特殊情況外不用關心;Exception是與程式設計打交道的基本異常型別,在Java類庫、使用者方法以及執行時故障中都可能丟擲Exception異常。所以Java程式設計師關心的基本型別通常是Exception。
異常的基本概念是用名稱代表發生的問題,異常的名稱可以望文生義。異常並非全在java.lang包中,還存在於util、net和io包中。
有一些問題屬於Java的標準執行時檢測的一部分,它們會自動被Java虛擬機器丟擲,所以不必在方法的異常說明中把它們列出來,這樣的異常被稱為不受檢查的異常(也有叫執行時異常的吧),它們都是RuntimeException的子類。這種異常屬於錯誤,不強制要求手動捕獲,可以再自己的程式碼中丟擲這種異常。如果不捕獲這種異常,它會穿越所有的執行路徑直達main()方法,在主程式退出前將呼叫異常的printStackTrace()方法。
RuntimeException代表的是程式設計錯誤:
(1)無法預料的錯誤。比如從你的程式碼控制範圍之外傳遞近來的Null引用;
(2)應該在程式碼中檢查的錯誤,比如陣列越界。
12.8
Java中的異常不允許回到異常丟擲的地點,如果想實現這一功能可以把try快放到迴圈中,這就建立了一個“程式繼續執行之前必須要達到”的條件,還可以加入一個static型別的計數器或者別的裝置,使迴圈在放棄以前能嘗試一定的次數。
當涉及到break和continue語句的時候,finally子句也會得到執行。
當try塊中包含return語句,其後的finally塊也會被執行。
12.8.3小節中作者演示了兩種不恰當的方法使得一些異常被忽略,值得注意
第一種問題的解決方法
12.9
當覆蓋方法時,只能丟擲在基類方法的異常說明中列出的異常,可以少丟擲或不丟擲這些異常,也可以丟擲這些異常的子類,或者不丟擲異常。但是不能新增新的異常。即某個方法的異常說明範圍可以變小但是不能變大。
異常限制對構造器不起作用。子類的構造器可以丟擲任何新異常。但是因為基類構造器必須以這樣或那樣的方法被呼叫,派生類建構函式的異常說明必須包含基類建構函式的異常說明。
派生類建構函式不能捕獲基類建構函式丟擲的異常。
12.10
對於丟擲異常的建構函式,作者認為應該這樣處理:在一個單獨try-catch語句中構造物件,一旦物件構造成功(即建構函式未丟擲異常)用另外的巢狀try-catch-finally語句寫其他功能,在finally中記得清理該物件的資源。
12.11
練習25中,子類覆蓋了父類的方法並且丟擲了比父類方法更窄的異常,當我們建立了一個子類的物件,並將其向上轉型為父類,呼叫該方法編譯器會強制要求捕獲父類的異常。
12.12
對於一些不知道怎麼處理的被檢查異常,作者推薦兩種辦法:
(1)在main()函式中丟擲這些異常;
(2)利用異常鏈,把被檢查異常包裝成RuntimeException丟擲
throw new RuntimeException(e);
補充(前兩條來自《Java 8程式設計參考官方教程》,第三條官方教程看不明白,參考了以下部落格http://blog.csdn.net/jackiehff/article/details/17839225,之前的例子理解了,最後的論述依舊不是很懂)
1.7版本,異常系統新增的三個新特性:
1、帶資源的try
這種特性有時被稱為自動資源管理(Automatic Resource Management,ARM)
try語句的形式:
try(資源定義和初始化) {
}
在資源定義和初始化語句中宣告的變數當try語句塊結束時,自動釋放資源。只有實現了AutoCloseable介面的資源才能使用帶資源的try語句,該介面定義了close()方法,try語句塊結束的時候會呼叫資源的close()方法。
try語句中宣告的資源被隱式的宣告為fianl,這意味著在建立資源變數後不能將其他變數賦值給該引用。另外,資源的作用域侷限於帶資源的try語句。
可以再一條try語句中管理多個資源。為此,只需要簡單的使用分號分隔每個資源即可。
關閉資源的close()方法也可能丟擲異常,使用帶資源的try語句時,當try語句塊中丟擲異常同時close()方法也丟擲異常,close()方法丟擲的異常會被抑制,但它並沒有丟失,而是被新增到第一個異常的抑制列表中,使用Throwable類定義的getSupperessed()方法可以獲取抑制異常列表。
2、多重捕獲
允許通過相同的catch子句捕獲多個異常,使用操作符 | 分隔每個異常。每個多重捕獲引數都被隱式的宣告為final,因此不能賦予它新的值。正常的捕獲並沒有final的限制,我想是因為多重捕獲形式如下:
catch(IOException | ArrayIndexOutOfBoundsException e ) {
}
如果要在catch語句塊中重新賦值,編譯器搞不清楚e到底是第一個型別還是第二個型別。
3、重新丟擲精確的異常
考慮下面的例子:
static class FirstException extends Exception { }
static class SecondException extends Exception { }
public void rethrowException(String exceptionName) throws Exception {
try {
if (exceptionName.equals("First")) {
throw new FirstException();
} else {
throw new SecondException();
}
} catch (Exception e) {
throw e;
}
}
這個例子的 try
塊既可以丟擲 FirstException
也可以丟擲 SecondException
。假定你想要在rethrowException方法宣告中的 throws
子句中指定異常型別為這兩種型別,在Java SE 7之前的版本中你不能這麼做。因為 catch
子句的異常引數e是 Exception
型別,並且 catch塊重新丟擲這個異常引數 e,所以你只能在rethrowException方法宣告中的 throws
子句中指定異常型別為 Exception
。
然而在Java SE 7中, 你可以在rethrowException方法宣告中的 throws
子句中指定異常型別為 FirstException
和 SecondException
。 Java SE 7編譯器可以探測到由 throw e
語句丟擲的異常必須來自於 try
塊, 並且 try
塊丟擲的異常只能是 FirstException
和 SecondException
。即使 catch
子句的異常引數e的型別是 Exception
,編譯器也可以探測到它是 FirstException
還是 SecondException
的例項:
public void rethrowException(String exceptionName)
throws FirstException, SecondException {
try {
// ...
}
catch (Exception e) {
throw e;
}
}
如果 catch
塊中的 catch
引數被指定給另一個值,那麼這種分析失效。然而,如果的 catch
引數被指定給另一個值, 你必須在方法宣告的 throws
子句中指定異常型別為 Exception
。
具體說來,在Java SE 7及後續版本中, 當你在一個 catch
子句中宣告一個或多個異常型別並且重新丟擲由這個 catch
塊處理的異常,編譯器會驗證重新丟擲的異常型別是否滿足以下條件:
try
塊可以丟擲它。- 先前的
catch
塊沒有辦法處理它。 - 它是
catch
子句其中一個異常引數的子類或者超類。