Java核心技術卷一 5. java異常、斷言和日誌
處理錯誤
由於出現錯誤而使得某些操作沒有完成,程序因該:
- 返回到一種安全狀態,並能夠讓用戶執行一些其他命令
- 允許用戶保存所有操作的結果,並以適當的方式終止程序
需要關註的問題:
- 用戶輸入錯誤
- 設備錯誤
- 物理限制
- 代碼錯誤
當某個方法不能夠采用正常的路徑完成它的任務,就可以通過另外一個一個路徑退出方法。這種情況下,方法並不返回任何值,而是拋出(throw)一個封裝了錯誤信息的對象。要註意這個方法將會立刻退出,並不返回任何值。調用這個方法的代碼也將無法繼續執行,異常處理機制開始搜索能夠處理這種異常狀況的異常處理器(exception handler)。
異常分類
全都派生於 Throwable 類,分解為兩個分支層次結構:Error 和 Exception
- Error 類層次結構描述 Java 運行時系統的內部錯誤和資源耗盡錯誤。應用程序不該拋出這種錯誤。
- Exception 類層次結構分為兩個分支。
- 派生於 RuntimeException 異常,由程序錯誤導致的異常。
- 其他不是程序導致的異常。
派生於 RuntimeException 異常的幾種情況:
- 錯誤的類型轉換
- 數組訪問越界,
ArrayIndexOutOfBoundsException
異常 - 訪問空指針,
NullPointerException
異常
其他異常的幾種情況:
- 試圖在文件尾部後面讀取數據
- 試圖打開一個不存在的文件
- 試圖根據給定的字符串查找 Class 對象,而這個字符串表示的類並不存在
Java 將派生於 Error 類或 RuntieException 類額所有異常成為非受查異常,其他的異常成為受查異常。
聲明受查異常
一個方法不僅要告訴編譯器要返回什麽值,還要告訴編譯器可能發生什麽錯誤。
例子:
public FileInputStream(String name) throws FileNotFoundException {
...
}
這個聲明表示可能拋出異常,如果發生了這個異常構造器將不會初始化一個新的 FileInputStream 對象,而是拋出一個 FileNotFoundException 類對象。拋出異常類對象後,運行時系統就會搜索異常處理器,以便指定如何處理 FileNotFoundException 對象。
需要拋出異常的情況:
- 調用一個拋出受查異常的方法。
- 程序運行過程中發現錯誤,利用 throw 語句拋出一個受查異常。
- 程序出現錯誤,一些非受查對象。
- Java 虛擬機和運行時庫出現的內部錯誤。
前兩種情況,必須告訴調用這個方法的程序員有可能拋出異常。防止程序遇到異常停止線程。
根據異常規範在方法首部聲明這個方法可能拋出的異常,多個受查異常用逗號隔開:
class MyAnimation{
...
public Image loadImage(String s) throws IOException, FileNotFoundException {
...
}
}
如果方法沒有聲明受查異常,編譯器會發出一個錯誤信息。
除了聲明異常外,還可以捕獲異常,拋出讓異常處理器處理。
另外:聲明一個異常,可能拋出這個異常的子類異常。
拋出異常
拋出異常的語句:
throw new EOFException();
String readData(Scanner in) throws EOFException {
...
while (...) {
if (!in.hasNext()) { //EOF encountered
if(n < len) throw new EOFException();
}
}
}
拋出以存在的異常類:
- 找到一個合適的異常類
- 創建這個類的一個對象
- 將對象拋出
一旦方法拋出了異常,這個方法就不可能返回到調用者。
創建異常類
標準異常無法充分的描述清楚問題,可以創建自己的異常類:
class FileFormatException extends IOException {
public FileFormatException(){}
public FileFormatException(String gripe){
super(gripe);//構造一個帶描述信息的異常
}
}
//拋出自己定義的異常類型
String readDate(BufferedReader in) throws FileFormatException {
...
while (...) {
if (ch == -1) { //EOF encounteered
if (n < len) throw new FileFormatException();
}
...
}
return s;
}
api:
//java.lang.Throwable
Throwable() 構造一個 Throwable 對象,沒有描述信息。
Throwable(String message) 構造一個 Throwable 對象,帶描述信息。
String getMessage() 獲取描述信息。
# 捕獲異常
捕獲異常
異常發生,沒有捕獲,程序就會終止執行,在控制臺打印異常信息。
捕獲一行,必須設置 try/catch 語句塊:
try {
code
more code
} catch (ExceptionType e) {
handler for this type
}
如果 try 內有代碼拋出了 catch 中定義的異常:
- 程序將跳過 try 語句塊的其余代碼。
- 程序將執行 catch 子句中的處理器代碼。
註意:如果 try 內的異常 catch 中沒有,則程序終止。
將異常傳遞給調用者:
public void read(String filename) throws IOException {...}
將不知道怎樣處理的異常繼續進行傳遞,傳遞異常使用 throws 說明符,告知調用者這個方法可能出現異常。
例外:如果編寫一個覆蓋超類的方法,這個方法沒有拋出異常,那麽這個方法必須捕獲方法代碼中出現的每一個受查異常。不允許在子類的 throws 說明符中出現超過超類方法所列出的異常類範圍。
捕獲多個異常
有兩種方式,第一種方式為每個異常類使用單獨的 catch 子句。
try {
code;
} catch (FileNotFoundException e) {
emergency action;
} catch (UnknownHostException e) {
emergency action;
} catch (IOException e) {
emergency action;
}
第二種方式可以對動機一樣的異常進行合並 catch 子句。
try {
code;
} catch (FileNotFoundException | UnknownHostException | IOException e) {
emergency action;
}
獲得對象的更多信息,可以:
e.getMessage();
e.getClass().getName();
註意:捕獲多個異常時,異常變量隱含為 final 變量,不可修改。
再次拋出異常與異常鏈
有些異常我們並不想指定發送錯誤的細節原因,但希望明確的指定它是否有問題:
try {
access the database
} catch (SQLException e) {
throw new ServletException("database error:" + e.getMessage());
}
//更好的方法,將原始異常設置為新異常的“原因”
try {
access the database
} catch (SQLException e) {
Throwable se = new ServletException("database error");
se.initCause(e);
throw se;
}
//重新得到原始異常
Throwable e = se.getCause();
這個受查異常,不允許拋出它,包裝技術十分有用,可以捕獲這個受查異常,把它包裝成一個運行時異常。
finally 子句
不管是否有異常被捕獲,finally 子句中的代碼都被執行:
InputStream in = new FileInputStream(...);
try {
core 1;
core 2;
} catch (IOException e) {
core 3;
} finally {
core 4;
}
會遇到3種情況:
- 代碼沒有異常。執行完 try 塊,然後執行 finally 塊。
- 拋出一個異常。執行 try 塊知道異常為止,跳過剩余 try 代碼,轉去執行與異常匹配的 catch 子句中的代碼,最後執行 finally 子句中的代碼。
- 代碼拋出異常,但是 catch 沒有匹配的異常。執行 try 塊知道異常為止,跳過剩余 try 代碼,轉去執行 finally 子句中的代碼,並將異常拋給這個方法的調用者。
解耦合try/catch
和try/finally
,提高代碼的清晰度:
try {
try {
code;
} finally {
in.close();
}
} catch (IOException e) {
show error message;
}
內層確保關閉輸入流;外層確保報告出現的錯誤。外層也會報告 finally 子句中出現的錯誤。
return的各種場景:
- return 在 try 塊中時,方法返回前 finally 子句的內容將被執行。
- return 在 try 塊 finally 子句中都存在時,finally 子句中的返回值將會覆蓋原始的返回值。
finally子句的壞處
try {
code 1;
} finally {
in.close();
}
如果 try 語句拋出了一些非 IOEception 的異常,這些異常只有調用者才能處理。執行 finally 語句塊,並調用 close 方法,有可能拋出 IOException 異常。有這種情況時,原始的異常將會丟失,轉而拋出 close 方法的異常。
帶資源的 try 語句
資源屬於實現了 AutoCloseable 接口的類時:
//接口的一個方法
void close() throws Exception
//最簡形式
try (Resource res = ...) {
work with res;
}
try 塊退出時,會自動調用 res.close() 。並且可以指定多個資源。
分析堆棧軌跡元素
堆棧軌跡元素是一個方法調用過程的列表,包含了程序執行過程中方法調用的特定位置。
調用 Throwable 類的 printStackTrace 方法訪問堆棧軌跡的文本描述信息。
使用異常機制的技巧
- 異常處理不能代替簡單的測試
- 不要過分地細化異常
- 利用異常層次結構
- 不要壓制異常
- 在檢測錯誤時,“苛刻”要比放任更好
- 不要羞於傳遞異常
使用斷言
斷言的概念
斷言機制允許在測試期間向代碼插入一些檢查語句。當代碼發布時,這些插入的檢測語句會被自動地移走。
assert 條件;
和
assert 條件 : 表達式;
這兩種形式都會對條件進行檢測, 如果結果為 false,則拋出一個 AssertionError 異常。 在第二種形式中,表達式將被傳人 AssertionError 的構造器,並轉換成一個消息字符串。 表達式部分的目的是產生一個消息字符串。
例子,斷言 x 是一個非負數值:
assert x >= 0;
assert x >= 0 : x;
啟用和禁用斷言
啟用:java -enableassertions MyApp
啟用某個類中的斷言:java -ea:MyClass -ea:com.mycompany.mylib... MyApp
禁用特定類和包的斷言:java -ea:... -da:MyClass MyApp
不能應用沒有類加載器的系統類上,要使用:-enablesystemassertions/-esa
使用斷言完成參數檢查
使用斷言的場景:
- 斷言失敗是致命的、不可恢復的錯誤
- 斷言用於開發和測試階段
不可通告可恢復性的錯誤,不該作為程序向用戶通告問題的手段。
不允許用 null 數組調用這個方法,並在這個方法的開頭使用斷言:assert a != null;
為文檔假設使用斷言
if (i % 3 == 0) ...
else if (i % 3 == 1) ...
else //i % 3 ==2
assert i >= 0;
if (i % 3 == 0) ...
else if (i % 3 == 1) ...
else assert i % 3 ==2;
記錄日誌
基本日誌
使用全局日誌記錄器並調用 info 方法:
Logger.getGlobal().info("File->Open menu item selected");
內容:
May 10, 2013 10:23:43 PM LoggingImageViewer fileOpen
INFO: File->Open menu item selected
在適當的地方(如 main 開始)以下將會取消所有日誌:
Logger.getGlobal().setLevel(Level.OFF);
調試技巧
- 打印任意變量的值:
System.out.println("x=" + x);
- 在每個類中放置一個單獨的 main 方法,做單元測試。
- 可以使用 JUnit 單元測試框架。
Java核心技術卷一 5. java異常、斷言和日誌