1. 程式人生 > >Java核心技術卷一 5. java異常、斷言和日誌

Java核心技術卷一 5. java異常、斷言和日誌

form 什麽 表示 ase ... per 分解 new 測試

處理錯誤

由於出現錯誤而使得某些操作沒有完成,程序因該:

  • 返回到一種安全狀態,並能夠讓用戶執行一些其他命令
  • 允許用戶保存所有操作的結果,並以適當的方式終止程序

需要關註的問題:

  1. 用戶輸入錯誤
  2. 設備錯誤
  3. 物理限制
  4. 代碼錯誤

當某個方法不能夠采用正常的路徑完成它的任務,就可以通過另外一個一個路徑退出方法。這種情況下,方法並不返回任何值,而是拋出(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 對象。

需要拋出異常的情況:

  1. 調用一個拋出受查異常的方法。
  2. 程序運行過程中發現錯誤,利用 throw 語句拋出一個受查異常。
  3. 程序出現錯誤,一些非受查對象。
  4. 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();
        }
    }
}

拋出以存在的異常類:

  1. 找到一個合適的異常類
  2. 創建這個類的一個對象
  3. 將對象拋出

一旦方法拋出了異常,這個方法就不可能返回到調用者。

創建異常類

標準異常無法充分的描述清楚問題,可以創建自己的異常類:

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 中定義的異常:

  1. 程序將跳過 try 語句塊的其余代碼。
  2. 程序將執行 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種情況:

  1. 代碼沒有異常。執行完 try 塊,然後執行 finally 塊。
  2. 拋出一個異常。執行 try 塊知道異常為止,跳過剩余 try 代碼,轉去執行與異常匹配的 catch 子句中的代碼,最後執行 finally 子句中的代碼。
  3. 代碼拋出異常,但是 catch 沒有匹配的異常。執行 try 塊知道異常為止,跳過剩余 try 代碼,轉去執行 finally 子句中的代碼,並將異常拋給這個方法的調用者。

解耦合try/catchtry/finally,提高代碼的清晰度:

try {
    try {
        code;
    } finally {
        in.close();
    }
} catch (IOException e) {
    show error message;
}

內層確保關閉輸入流;外層確保報告出現的錯誤。外層也會報告 finally 子句中出現的錯誤。

return的各種場景

  1. return 在 try 塊中時,方法返回前 finally 子句的內容將被執行。
  2. 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 方法訪問堆棧軌跡的文本描述信息。

使用異常機制的技巧

  1. 異常處理不能代替簡單的測試
  2. 不要過分地細化異常
  3. 利用異常層次結構
  4. 不要壓制異常
  5. 在檢測錯誤時,“苛刻”要比放任更好
  6. 不要羞於傳遞異常

使用斷言

斷言的概念

斷言機制允許在測試期間向代碼插入一些檢查語句。當代碼發布時,這些插入的檢測語句會被自動地移走。

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);

調試技巧

  1. 打印任意變量的值:System.out.println("x=" + x);
  2. 在每個類中放置一個單獨的 main 方法,做單元測試。
  3. 可以使用 JUnit 單元測試框架。

Java核心技術卷一 5. java異常、斷言和日誌