天天看點

Java Review(三十二、異常處理)異常分類捕捉異常抛出異常Java的異常跟蹤棧使用異常機制的技巧

文章目錄

異常機制己經成為判斷一門程式設計語言是否成熟的标準,除傳統的像 C 語言沒有提供異常機制之外,目前主流的程式設計語言如 Java、 C# 、 Ruby、 Python 等都提供了成熟的異常機制 。 異常機制可以使程式中的異常處理代碼和正常業務代碼分離 ,保證程式代碼更加優雅,并可以提高程式的健壯性 。

在 Java 程式設計語言中, 異常對象都是派生于 Throwable 類的一個執行個體。

圖 一 是 Java 異常層次結構的一個簡化示意圖。

圖一:Java 異常層次結構

Java Review(三十二、異常處理)異常分類捕捉異常抛出異常Java的異常跟蹤棧使用異常機制的技巧

需要注意的是,所有的異常都是由 Throwable 繼承而來,但在下一層立即分解為兩個分支:Error 和 Exception:

Error 類層次結構描述了 Java 運作時系統的内部錯誤和資源耗盡錯誤。 應用程式不應該抛出這種類型的對象。 如果出現了這樣的内部錯誤, 除了通告給使用者,并盡力使程式安全地終止之外, 再也無能為力了。這種情況很少出現。

在設計 Java 程式時, 需要關注 Exception 層次結構。 這個層次結構又分解為兩個分支:

Checked異常和 Runtime 異常 (運作時異常) 。 所有的 RuntimeException類及其子類的執行個體被稱為 Runtime 異常:不是 RuntimeException 類及其子類的異常執行個體則被稱為Checked 異常 。

圖二:Java 常見異常類層次結構

Java Review(三十二、異常處理)異常分類捕捉異常抛出異常Java的異常跟蹤棧使用異常機制的技巧

隻有 Java 語言提供了 Checked 異常,其他語言都沒有提供 Checked 異常 。 Java 認為 Checked 異常都是可以被處理(修複〉的異常,是以 Java 程式必須顯式處理 Checked 異常 。 如果程式沒有處理 Checked異常,該程式在編譯時就會發生錯誤,無法通過編譯。

Checked 異常展現了 Java 的設計哲學一一沒有完善錯誤處理的代碼根本就不會被執行 !

Java 語言規範将派生于Error 類 或 RuntimeException 類的所有異常稱為非受檢( unchecked ) 異常,所有其他的異常稱為受檢( checked) 異常。 編譯器将核查是否為所有的受査異常提供了異常處理器。

使用 try… . catch 捕獲異常

要想捕獲一個異常, 必須設定 try/catch語句塊。最簡單的 try語句塊如下所示:

try{
  // 業務實作代碼
}catch (Exception e){
  alert 輸入不合法
   goto retry
}      

如果在 try語句塊中的任何代碼抛出了一個在 catch 子句中說明的異常類, 那麼:

  • 程式将跳過 try語句塊的其餘代碼。
  • 程式将執行 catch 子句中的處理器代碼。

如果在 try 語句塊中的代碼沒有拋出任何異常,那麼程式将跳過 catch 子句。

如果方法中的任何代碼拋出了一個在 catch 子句中沒有聲明的異常類型,那麼這個方法就會立刻退出(希望調用者為這種類型的異常設記了catch 子句。

如下是一個典型的捕獲異常示例:

public void read(String filename) {
try{
   InputStream in = new Filei叩utStream(filename);
   int b;
   while ((b = in.read()3 != -1) {
   process input
   }
 }catch (IOException exception) {
  exception.printStackTrace();
 }       

在一個 try 語句塊中可以捕獲多個異常類型,并對不同類型的異常做出不同的處理。可以按照下列方式為每個異常類型使用一個單獨的 catch 子句:

try{
 code that might throwexceptions
}catch (FileNotFoundException e) {
  emergencyactionfor missingfiles
}catch (UnknownHostException e) {
  emergency actionfor unknown hosts
}catch (IOException e) {
  emergencyactionfor all other I/O problems
}        

異常對象可能包含與異常本身有關的資訊。要想獲得異常對象的更多資訊, 可以使用以下幾個方法:

  • getMessage(): 傳回該異常的較長的描述字元串 。
  • printStackTrace() : 将該異常的跟蹤枝資訊輸出到标準錯誤輸出 。
  • printStackTrace(PrintS仕eam s): 将該異常的跟蹤枝資訊輸出到指定輸出流 。
  • getStackTrace() : 傳回該異常的跟蹤枝資訊 。

可以使用

e.getClass().getName()      

得到異常對象的實際類型。

在 Java SE 7中,同一個 catch 子句中可以捕獲多個異常類型。例如,假設對應缺少檔案和未知主機異常的動作是一樣的,就可以合并 catch 子句:

try{
 code that might throw exceptions
}catch (FileNotFoundException | UnknownHostException e) {
  emergency action for missing files and unknown hosts
}catch (IOException e) {
  emergency action for all other I/O problems
}        

使用一個 catch 塊捕獲多種類型的異常時需要注意如下兩個地方 :

  • 捕獲多種類型的異常時 , 多種異常類型之間用豎線 (|) 隔開。
  • 捕獲多種類型的異常時 , 異常變量有隐式的 final 修飾,是以程式不能對異常變量重新指派。

當代碼抛出一個異常時, 就會終止方法中剩餘代碼的處理,并退出這個方法的執行。如果方法獲得了一些本地資源,并且隻有這個方法自己知道,又如果這些資源在退出方法之前必須被回收,那麼就會産生資源回收問題。一種解決方案是捕獲并重新抛出所有的異常,這種解決方案并不完美,這是因為需要在兩個地方清除所配置設定的資源。一個在正常的代碼中;另一個在異常代碼中。

Java 有一種更好的解決方案,這就是 finally 子句。下面将介紹 Java 中如何恰當地關閉一個檔案。如果使用 Java 編寫資料庫程式,就需要使用同樣的技術關閉與資料庫的連接配接。當發生異常時,關閉所有資料庫的連接配接是非常重要的。不管是否有異常被捕獲,finally 子句中的代碼都被執行。在下面的示例中, 程式将在所

有情況下關閉檔案:

InputStream in = new FileInputStream(. . .);
try{
   code that might throwexceptions
}catch (IOException e) { // 3
    showerror message
   // 4
}finally{ // 5
    in.close();
}          

在上面這段代碼中,有下列 3 種情況會執行 finally 子句:

1 ) 代碼沒有抛出異常。 在這種情況下, 程式首先執行 try 語句塊中的全部代碼,然後執行 finally 子句中的代碼t 随後, 繼續執行 try 語句塊之後的第一條語句。也就是說,執行标注的 1、 2、 5、 6 處。

2 ) 抛出一個在 catch 子句中捕獲的異常。在上面的示例中就是 IOException 異常。在這種情況下,程式将執行 try語句塊中的所有代碼,直到發生異常為止。此時,将跳過 try語句塊中的剩餘代碼,轉去執行與該異常比對的 catch 子句中的代碼, 最後執行 finally 子句中的代碼。如果 catch 子句沒有抛出異常,程式将執行 try 語句塊之後的第一條語句。在這裡,執行标注 1、 3、 4、5、 6 處的語句。如果 catch 子句抛出了一個異常, 異常将被抛回這個方法的調用者。在這裡, 執行标注1、 3、 5 處的語句。

3 ) 代碼抛出了一個異常, 但這個異常不是由 catch 子句捕獲的。在這種情況下,程式将執行 try 語句塊中的所有語句,直到有異常被抛出為止。此時, 将跳過 try 語句塊中的剩餘代碼, 然後執行 finally 子句中的語句, 并将異常抛給這個方法的調用者。在這裡, 執行标注 1、 5 處的語句。

try 語句可以隻有 finally 子句,而沒有 catch 子句。例如,下面這條 try 語句:

InputStream in = . .
try{
  code that might throwexceptions
}finally{
  in.close();
}        

警告:當 finally 子句包含 return 語句時, 将會出現一種意想不到的結果„ 假設利用 return語句從 try語句塊中退出。在方法傳回 前,finally 子句的内容将被執行。如果 finally 子句中也有一個 return 語句,這個傳回值将會覆寫原始的傳回值。如:

public static int f(int n) {

  try{

  int r = n * n;

  return r;

  }finally{

  if (n = 2) return 0;

  }

  }

如果調用 f(2), 那麼 try 語句塊的計算結果為 r = 4, 并執行 return 語句然而,在方法真正傳回前,還要執行 finally 子句。finally 子句将使得方法傳回 0, 這個傳回值覆寫了原始的傳回值 4

使用throws聲明抛出異常的思路是:目前方法不知道如何處理這種類型的異常,該異常應該由上一級調用者處理;如果main方法也不知道如何處理該類型的異常,也可以使用throws聲明抛出異常,該異常交給JVM處理,JVM對異常的處理方法是:列印異常的跟蹤棧資訊,并終止程式運作。

throws隻能在方法簽名中使用,throws可以聲明抛出多個異常類,多個異常類之間以逗号隔開:

throws ExceptionClass1,ExceptionClass2 …………      

ThrowsTest.java

public class ThrowsTest{
    public static void main(String[] args) throws Exception{
        // 因為test()方法聲明抛出IOException異常,
        // 是以調用該方法的代碼要麼處于try...catch塊中,
        // 要麼處于另一個帶throws聲明抛出的方法中。
        test();
    }
    public static void test()throws IOException{
        // 因為FileInputStream的構造器聲明抛出IOException異常,
        // 是以調用FileInputStream的代碼要麼處于try...catch塊中,
        // 要麼處于另一個帶throws聲明抛出的方法中。
        FileInputStream fis = new FileInputStream("a.txt");
    }
}      

如果需要在程式中自行抛出異常,則應使用 throw 語句。throw 吾句可以單獨使用,throw 語句抛出的不是異常類,而是一個異常執行個體,而且每次隻能抛出一個異常實 throw 語句的文法格式如下:

throw ExceptionInstance ;      

ThrowTest.java

public class ThrowTest{
    public static void main(String[] args){
        try{
            // 調用聲明抛出Checked異常的方法,要麼顯式捕獲該異常
            // 要麼在main方法中再次聲明抛出
            throwChecked(-3);
        }catch (Exception e){
            System.out.println(e.getMessage());
        }
        // 調用聲明抛出Runtime異常的方法既可以顯式捕獲該異常,
        // 也可不理會該異常
        throwRuntime(3);
    }
    public static void throwChecked(int a)throws Exception{
        if (a > 0){
            // 自行抛出Exception異常
            // 該代碼必須處于try塊裡,或處于帶throws聲明的方法中
            throw new Exception("a的值大于0,不符合要求");
        }
    }
    public static void throwRuntime(int a){
        if (a > 0){
            // 自行抛出RuntimeException異常,既可以顯式捕獲該異常
            // 也可完全不理會該異常,把該異常交給該方法調用者處理
            throw new RuntimeException("a的值大于0,不符合要求");
        }
    }
}      

在程式中,可能會遇到任何标準異常類都沒有能夠充分地描述清楚的問題。 在這種情況下,可以自定義異常類。

是定義一個派生于Exception 的類,或者派生于 Exception 子類的類。例如, 定義一個派生于 IOException 的類。

習慣上, 定義的類應該包含兩個構造器, 一個是預設的構造器;另一個是帶有較長的描述資訊的構造器(超類 Throwable 的 toString 方法将會列印出這些詳細資訊, 這在調試中非常有用)。

class FileFormatException extends IOException{
   public FileFormatExceptionO {}
   public FileFormatException(String gripe) {
     super(gripe);
   }        

接下來,就可以抛出自定義的異常類型:

String readData(BufferedReader in) throws FileFormatException{ 
 while (. . .) {
  if (ch == -1){ // EOF encountered
    if (n < len){
     throw new FileFornatException();
    }
  }
} 
          

對于真實的企業級應用而言,常常有嚴格的分層關系,層與層之間有非常清晰的劃分,上層功能的實作嚴格依賴于下 API,也不會跨層通路:

圖三:MVC三層結構

Java Review(三十二、異常處理)異常分類捕捉異常抛出異常Java的異常跟蹤棧使用異常機制的技巧

當業務邏輯層通路持久層出現

SQLException 異常時, 程式不應該把底層的 SQLException 異常傳到使用者界面,

有如下兩個原因。

  • 對 于 正 常 用 戶 而 言 , 他 們不想看到底層 SQLException異常,SQLException 異常對他們使用該系統沒有任何幫助。
  • 對于惡意使用者而言, 将 SQLException 異常暴露出來不安全

把底層的原始異常直接傳給使用者是一種不負責任的表現。 通常的做法是:程式先捕獲原始異常, 然後抛出一個新的業務異常, 新的業務異常中包含了對使用者的提示資訊, 這種處理方式被稱為異常轉譯。 假設程式需要實作工資計算的方法,

則程式應該采用如下結構的代碼來實作該方法:

public void calSal() throws SalException{
  try{
   // 實作結算工資的業務邏輯
  }catch(SQLException sqle){
   // 把原始異常記錄下來, 留給管理者
   // 下面異常中的 message 就是對使用者的提示
     throw new SalException("通路底層資料庫出現異常");
  } catch(Exception e)
    // 把原始異常記錄下來, 留給管理者
   // 下面異常中的 message 就是對使用者的提示
    throw new SalException( "系統出現未知異常");
  }
}          

異常對象的 printStackTrace()方法用于列印異常的跟蹤棧資訊, 根據 printStackTrace()方法的輸出結果, 開發者可以找到異常的源頭, 并跟蹤到異常一路觸發的過程。

PrintStackTraceTest.java

class SelfException extends RuntimeException
{
    SelfException(){}
    SelfException(String msg)
    {
        super(msg);
    }
}
public class PrintStackTraceTest
{
    public static void main(String[] args)
    {
        firstMethod();
    }
    public static void firstMethod()
    {
        secondMethod();
    }
    public static void secondMethod()
    {
        thirdMethod();
    }
    public static void thirdMethod()
    {
        throw new SelfException("自定義異常資訊");
    }
}      

運作結果:

Java Review(三十二、異常處理)異常分類捕捉異常抛出異常Java的異常跟蹤棧使用異常機制的技巧

異常從thirdMethod方法開始觸發 , 傳到 secondMethod 方法,再傳到firstMethod 方法, 最後傳到 main 方法, 在 main 方法終止, 這個過程就是 Java 的異常跟蹤棧。

面向對象的應用程式運作時, 經常會發生一系列方法調用, 進而形成“ 方法調用棧”, 異常的傳播則相反: 隻要異常沒有被完全捕獲( 包括異常沒有被捕獲, 或異常被處理後重新抛出了新異常),異常從發生異常的方法逐漸向外傳播, 首先傳給該方法的調用者, 該方法調用者再次傳給其調用者……直至最後傳到 main 方法, 如果 main 方法依然沒有處理該異常, JVM 會中止該程式, 并列印異常的跟蹤棧資訊。

圖中所示的異常跟蹤棧資訊非常清晰——它記錄了應用程式中執行停止的各個點:

第一行的資訊詳細顯示了異常的類型和異常的詳細消息。

接下來跟蹤棧記錄程式中所有的異常發生點, 各行顯示被調用方法中執行的停止位置, 并标明類、類中的方法名、 與故障點對應的檔案的行。

一行行地往下看, 跟蹤棧總是最内部的被調用方法逐漸上傳,直到最外部業務操作的起點, 通常就是程式的入口 main 方法或 Thread 類的 rim 方法( 多線程的情形)。

下面給出使用異常機制的幾個技巧:

作為一個示例, 在這裡編寫了一段代碼, 試着上百萬次地對一個空棧進行退棧操作。在實施退棧操作之前, 首先要檢視棧是否為空。

if (!s.empty()) s.popO;      

接下來,強行進行退棧操作。然後, 捕獲 EmptyStackException 異常來告知我們不能這樣做。

try{ 
  s.pop();
 }catch (EmptyStackException e) {
}      

在測試的機器上, 調用 isEmpty 的版本運作時間為 646 毫秒。捕獲 EmptyStackException 的版本運作時間為 21 739 毫秒。

可以看出,與執行簡單的測試相比, 捕獲異常所花費的時間大大超過了前者, 是以使用異常的基本規則是:隻在異常情況下使用異常機制。

很多程式員習慣将每一條語句都分裝在一個獨立的 try語句塊中。

PrintStream out;
Stack s;
for (i = 0;i < 100; i++) {
try
{ n = s.pop(); }
catch (EmptyStackException e) {
II stack was empty
}
try
{
out.writelnt(n); }
catch (IOException e) {
ff problem writing to file
} }      

這種程式設計方式将導緻代碼量的急劇膨脹。首先看一下這段代碼所完成的任務。在這裡,希望從棧中彈出 100 個數值, 然後将它們存人一個檔案中。如果棧是空的, 則不會變成非空狀态;如果檔案出現錯誤, 則也很難給予排除。出現上述問題後,這種程式設計方式無能為力。是以,有必要将整個任務包裝在一個 try語句塊中,這樣, 當任何一個操作出現問題時, 整個任務都可以取消。

try
{
for (i = 0; i < 100; i++) { n = s.popO ;
out.writelnt(n); } }
catch (IOException e) { // problem writing to file
}
catch (EmptyStackException e) { f] stack was empty
}      

這段代碼看起來清晰多了。這樣也滿足了異常處理機制的其中一個目标,将正常處理與錯誤處理分開。

不要隻抛出 RuntimeException 異常。應該尋找更加适當的子類或建立自己的異常類。不要隻捕獲 Thowable 異常, 否則,會使程式代碼更難讀、 更難維護。

考慮受查異常與非受查異常的差別。 已檢查異常本來就很龐大,不要為邏輯錯誤抛出這些異常。(例如, 反射庫的做法就不正确。 調用者卻經常需要捕獲那些早已知道不可能發生的異常。)

将一種異常轉換成另一種更加适合的異常時不要猶豫。例如, 在解析某個檔案中

的 一 個 整 數 時, 捕 獲 NumberFormatException 異 常, 然 後 将 它 轉 換 成 IOException 或MySubsystemException 的子類。

在 Java 中,往往強烈地傾向關閉異常。如果編寫了一個調用另一個方法的方法,而這個方法有可能 100 年才抛出一個異常, 那麼, 編譯器會因為沒有将這個異常列在 throws 表中産生抱怨。而沒有将這個異常列在 throws 表中主要出于編譯器将會對所有調用這個方法的方法進行異常處理的考慮。是以,應該将這個異常關閉:

public Image loadImage(String s) {
try
{ // code that threatens to throw checked exceptions
}
catch (Exception e) {} // so there
}      

現在,這段代碼就可以通過編譯了。除非發生異常,否則它将可以正常地運作。即使發生了異常也會被忽略。如果認為異常非常重要,就應該對它們進行處理。

當檢測到錯誤的時候, 有些程式員擔心抛出異常。在用無效的參數調用一個方法時,傳回一個虛拟的數值, 還是抛出一個異常, 哪種處理方式更好? 例如, 當棧空時,Stack.pop 是傳回一個 null, 還是抛出一個異常? 我們認為:在出錯的地方抛出一個 EmptyStackException異常要比在後面抛出一個NullPointerException 異常更好。

很多程式員都感覺應該捕獲抛出的全部異常。如果調用了一個抛出異常的方法,例如,FilelnputStream 構造器或 readLine 方法,這些方法就會本能地捕獲這些可能産生的異常。其 實, 傳遞異常要比捕獲這些異常更好:

public void readStuff(String filename) throws IOException // not a sign of shame! {
  InputStreaa in = new FilelnputStream(filename);
  ……
}        

讓高層次的方法通知使用者發生了錯誤, 或者放棄不成功的指令更加适宜。

規則 5、6 可以歸納為“早抛出,晚捕獲"

參考:

【1】:《Java核心技術 卷一》

【2】:《瘋狂Java講義》