1 Java的異常控制機制

捕獲錯誤最理想的是在編譯期,最好在試圖運行程序以前。然而,并非所有錯誤都能在編譯期間偵測到。有些問題必須在運行期間解決。讓錯誤的締結者通過一定的方法預先向接收者傳遞一些適當的信息,使其知道可能發生什么樣的錯誤以及該如何處理遇到的問題,這就是Java的異常控制機制。
“異常”(Exception)這個詞表達的是一種正常情況之外的“異常”。在問題發生的時候,我們可能不知具體該如何解決,但肯定知道已不能不顧一切地繼續下去。此時,必須堅決地停下來,并由某人、某地指出發生了什么事情,以及該采取何種對策。異常機制的另一項好處就是能夠簡化錯誤控制代碼。我們再也不用檢查一個特定的錯誤,然后在程序的多處地方對其進行控制。此外,也不需要在方法調用的時候檢查錯誤(因為保證有人能捕獲這里的錯誤)。我們只需要在一個地方處理問題:“異常控制模塊”或者“異常控制器”。這樣可有效減少代碼量,并將那些用于描述具體操作的代碼與專門糾正錯誤的代碼分隔開。一般情況下,用于讀取、寫入以及調試的代碼會變得更富有條理。
若某個方法產生一個異常,必須保證該異常能被捕獲,并獲得正確對待。Java的異常控制機制的一個好處就是允許我們在一個地方將精力集中在要解決的問題上,然后在另一個地方對待來自那個代碼內部的錯誤。那個可能發生異常的地方叫做“警戒區”,它是一個語句塊,我們有必要派遣警探日夜監視著。生成的異常必須在某個地方被捕獲和進行處理,就象警察抓到嫌疑犯后要帶到警署去詢問。這個地方便是異常控制模塊。
“警戒區”是一個try關鍵字開頭后面用花括號括起來的語句塊,我們把它叫作“try塊”。當try塊中有語句發生異常時就擲出某種異常類的一個對象。異常被異常控制器捕獲和處理,異常控制器緊接在try塊后面,且用catch關鍵字標記,因此叫做“catch塊”。catch塊可以有多個,每一個用來處理一個相應的異常,因為在“警戒區”內可能發生的異常種類不止一個。所以,異常處理語句的一般格式是:
try {
  // 可能產生異常的代碼
  }
 catch (異常對象 e) {
   //異常 e的處理語句
 }catch (異常對象 e1) {
   //異常 e的處理語句
 }catch (異常對象 e2) {
   //異常 e的處理語句
 }

即使不使用try-catch結構,發生異常時Java的異常控制機制也會捕獲該異常,輸出異常的名稱并從異常發生的位置打印一個堆棧跟蹤。然后立即終止程序的運行。下面的例子發生了一個“零除”異常,后面的hello沒有被打印。

例1 沒有作異常控制的程序。

///
public
class Exception1 {
   
public static void
main(String args[]) {
   
int
b = 0;
   
int
a = 3 / b;
   
System.out.println( "Hello!"
);
 
}
}
///

輸出結果:
java.lang.ArithmeticException: / by zero
at Exception1.main(Exception1.java:5)
Exception in thread "main" Exit code: 1
There were errors

但是如果使用了try-catch來處理異常,那么在打印出異常信息后,程序還將繼續運行下去。下面是處理了的代碼。

///
// Exception2.java

public
class Exception2 {
 
public static void
main(String args[]) {
   try
 

   
int b = 0;  
    int
a = 3 / b;
    }
   catch(ArithmeticException e) {
     e.printStackTrace
    }

   
System.out.println( "Hello!" );
 
}
}
///

輸出結果:
Exception:
java.lang.ArithmeticException: / by zero
   at Exception2.main(Exception1.java:5)
Hello!

與前例不同的是,Hello!被輸出了。這就是try-catch結構的用處,它使異常發生和處理后程序得以“恢復”而不是“中斷”。

2 異常類、異常規范和throw語句

為了使異常控制機制更出色地發揮它的功效,Java設計者幾乎所以可能發生的異常,預制了各色各樣的異常類和錯誤類。它們都是從“可擲出”類Throwable繼承而來的,它派生出兩個類Error和Exception。由Error派生的子類命名為XXXError,其中詞XXX是描述錯誤類型的詞。由Exception派生的子類命名為XXXException,其中詞XXX是描述異常類型的詞。Error類處理的是運行使系統發生的內部錯誤,是不可恢復的,唯一的辦法只要終止運行運行程序。因此,客戶程序員只要掌握和處理好Exception類就可以了。
Exception類是一切異常的根。現成的異常類非常之多,我們不可能也沒有必要全部掌握它。好在異常類的命名規則大致描述出了該類的用途,而異常類的方法基本是一樣的。下面給出lang包中聲明的部分異常類。

RuntimeException            運行時異常
NullPointerException        數據沒有初始化就使用
IndexOutOfBoundsException   數組或字符串索引越界
NoSuchFieldException        文件找不到
NoSuchMethodException       方法沒有定義
ArithmeticException         非法算術運行

在其他包中也有相關的異常類,例如io包中有IOEception類。利用異常的命名規則,你可以使用下面的DOS命令在包所在的目錄查看有什么異常類可用:
    DIR *Eception.class 
對于運行時異常RuntimeException,我們沒必要專門為它寫一個異常控制器,因為它們是由于編程不嚴謹而造成的邏輯錯誤。只要讓出現終止,它會自動得到處理。需要程序員進行異常處理的是那些非運行期異常。
Throwable有三個基本方法:

  • String getMessage()   獲得詳細的消息。

  • String toString()     返回對本類的一段簡要說明,其中包括詳細的消息(如果有的話)。

  • void printStackTrace()  或  void printStackTrace(PrintStream)
    打印出調用堆棧路徑。調用堆棧顯示出將我們帶到異常發生地點的方法調用的順序。

因為Exception類是一切異常的根,所以對任何一個現有的異常類都可以使用上述方法。

異常規范 throws

java庫程序員為了使客戶程序員準確地知道要編寫什么代碼來捕獲所有潛在的異常,采用一種叫做throws的語法結構。它用來通知那些要調用方法的客戶程序員,他們可能從自己的方法里“擲”出什么樣的異常。這便是所謂的“異常規范”,它屬于方法聲明的一部分,即在自變量(參數)列表的后面加上throws 異常類列表。例如
    void f() throws tooBig, tooSmall, divZero { 方法體}
若使用下述代碼:
    void f() [ // ...
它意味著不會從方法里“擲”出異常(除類型為RuntimeException的異常以外,它可能從任何地方擲出)。
如果一個方法使用了異常規范,我們在調用它時必須使用try-catch結構來捕獲和處理異常規范所指示的異常,否則編譯程序會報錯而不能通過編譯。這正是Java的異常控制機制的杰出貢獻,它對可能發生的意外及早預防從而加強了代碼的健壯性。
在使用了異常規范的方法聲明中,庫程序員使用throw語句來擲出一個異常。throw語句的格式為:
    thrownew XXXException();
由此可見,throw語句擲出的是XXX類型的異常的對象(隱式的句柄)。而catch控制器捕獲對象時要給出一個句柄 catch(XXXException e)。
我們也可以采取“欺騙手段”,用throw語句“擲”出一個并沒有發生的異常。編譯器能理解我們的要求,并強迫使用這個方法的用戶當作真的產生了那個異常處理。在實際應用中,可將其作為那個異常的一個“占位符”使用。這樣一來,以后可以方便地產生實際的異常,毋需修改現有的代碼。下面我們用“欺騙手段”給出一個捕獲異常的示例程序。

例2 本例程演示異常類的常用方法。

///
public
class ExceptionMethods {
  publicstaticvoid main(String[] args) {
    try {
      thrownew Exception("Here's my Exception");
    } catch(Exception e) {
      System.out.println("Caught Exception");
      System.out.println(
        "e.getMessage(): " + e.getMessage());
      System.out.println(
        "e.toString(): " + e.toString());
      System.out.println("e.printStackTrace():");
      e.printStackTrace();
    }
  }
}
///

該程序輸出如下:
Caught Exception
e.getMessage(): Here's my Exception
e.toString(): java.lang.Exception: Here's my Exception
e.printStackTrace():
java.lang.Exception: Here's my Exception
        at ExceptionMethods.main

在一個try區中潛在的異常可能是多種類型的,那時我們需要用多個catch塊來捕獲和處理這些異常。但異常發生時擲出了某類異常對象,Java依次逐個檢查這些異常控制器,發現與擲出的異常類型匹配時就執行那以段處理代碼,而其余的不會被執行。為了防止可能遺漏了某一類異常控制器,可以放置一個捕獲Exception類的控制器。Exception是可以從任何類方法中“擲”出的基本類型。但是它必須放在最后一個位置,因為它能夠截獲任何異常,從而使后面具體的異常控制器不起作用。下面的示例說明了這一點。

例3 本例程演示多個異常控制器的排列次序的作用。

///
public
class MutilCatch {
  private staticvoid test(int
i) {
    try
{
   
int
x = i;
   
if
(x>0)
     
throw new ArithmeticException ( "this is a Arithmetic Exception!"
);
   
else if
(x<0)
     
throw new NullPointerException ( "this is a NullPointer Exception!"
);
   
else
   
   throw new Exception( "this is a Exception!" );

   
} catch (ArithmeticException e) {
   
    
System.out.println(e.toString());
   
} catch
(NullPointerException e) {
   
    
System.out.println(e.toString());
   
} catch
(Exception e) {
   
    
System.out.println(e.toString());
   
}

  
}
  public
static void main(String[] args) {
   
test(-1); test(0); test(1);
 
}
 
}
///

運行結果:
java.lang.NullPointerException: this is a NullPointer Exception!
java.lang.Exception: this is a Exception!
java.lang.ArithmeticException: this is a Arithmetic Exception!
如果你把捕獲Exception的catch放在前面,編譯就通不過。


3 用finally清理

我們經常會遇到這樣的情況,無論一個異常是否發生,必須執行某些特定的代碼。比如文件已經打開,關閉文件是必須的。但是,在try區內位于異常發生點以后的代碼,在發生異常后不會被執行。在catch區中的代碼在異常沒有發生的情況下不會被執行。為了無論異常是否發生都要執行的代碼,可在所有異常控制器的末尾使用一個finally從句,在finally塊中放置這些代碼。(但在恢復內存時一般都不需要,因為垃圾收集器會自動照料一切。)所以完整的異常控制結構象下面這個樣子:

try { 警戒區域 }
catch (A a1) { 控制器 A }
catch (B b1) { 控制器 B }
catch (C c1) { 控制器 C }
finally { 必須執行的代碼}

例4 演示finally從句的程序。

///
// FinallyWorks.java

// The finally clause is always executed
public class FinallyWorks {
  staticint count = 0;
  publicstaticvoid main(String[] args) {
    while(true) {
      try {
        // post-increment is zero first time:
        if(count++ == 0)
          thrownew Exception();
        System.out.println("No exception");
      } catch(Exception e) {
        System.out.println("Exception thrown");
      } finally {
        System.out.println("in finally clause");
        if(count == 2) break; // out of "while"
      }
    }
  }
}
///

運行結果:
Exception thrown
in finally clause
No exception
in finally clause
一開始count=0發生異常,然后進入finally塊;進入循環第二輪沒有異常,但又執行一次finally塊,并在其中跳出循環。
下面我們給出一個有的實用但較為復雜一點的程序。我們創建了一個InputFile的類。它的作用是打開一個文件,然后每次讀取它的一行內容。

例5 讀文本文件并顯示到屏幕上。

///
//: Cleanup.java

// Paying attention to exceptions in constructors
import java.io.*;

class InputFile {
  private BufferedReader in;
  InputFile(String fname) throws Exception {
    try {
      in = new BufferedReader(new FileReader(fname));
      // Other code that might throw exceptions
    } catch(FileNotFoundException e) {
      System.out.println("Could not open " + fname);
      // Wasn't open, so don't close it
      throw e;
    } catch(Exception e) {
      // All other exceptions must close it
      try {
        in.close();
      } catch(IOException e2) {
        System.out.println("in.close() unsuccessful");
      }
      throw e;
    } finally {
      // Don't close it here!!!
    }
  }
  String getLine() {
    String s;
    try {
      s = in.readLine();
    } catch(IOException e) {
      System.out.println("readLine() unsuccessful");
      s = "failed";
    }
    return s;
  }
  void cleanup() {
    try {
      in.close();
    } catch(IOException e2) {
      System.out.println("in.close() unsuccessful");
    }
  }
}
publicclass Cleanup {
  publicstaticvoid main(String[] args) {
    try {
      InputFile in = new InputFile("Cleanup.java");
      String s;
      int i = 1;
      while((s = in.getLine()) != null)
        System.out.println(""+ i++ + ": " + s);
      in.cleanup();
    } catch(Exception e) {
      System.out.println( "Caught in main, e.printStackTrace()");
      e.printStackTrace();
    }
  }
}
///

運行后輸出的前2行是:
1: //: Cleanup.java
2: // Paying attention to exceptions in constructors
3: import java.io.*;

簡要說明 InputFile的類包含一個構建器和兩個方法cleanup和getLine。構建器要打開一個文件fname,首先要捕獲FileNotFoundException類異常。在它的處理代碼中再擲出這個異常(throw e;)。在更高的控制器中試圖關閉文件,并捕捉關閉失敗的異常IOException。cleanup()關閉文件,getLine()讀文件的一行到字符串,它們都用了異常處理機制。Cleanup是主類,在main()中首先創建一個InputFile類對象,因為它的構建器聲明時用了異常規范,所以必須用try-catch結構來捕獲異常。

4 創建自己的異常類

雖然Java類庫提供了十分豐富的異常類型,能夠滿足絕大多數編程需要。但是,在開發較大的程序時,也有可能需要建立自己的異常類。要創建自己的異常類,必須從一個現有的異常類型繼承——最好在含義上與新異常近似。創建一個異常相當簡單,只要按如下格式寫兩個構建器就行:

class MyException extends Exception {
    public MyException() {}
    public MyException(String msg) {
    super(msg);
  }
}
這里的關鍵是“extends Exception”,它的意思是:除包括一個Exception的全部含義以外,還有更多的含義。增加的代碼數量非常少——實際只添加了兩個構建器,對MyException的創建方式進行了定義。請記住,假如我們不明確調用一個基礎類構建器,編譯器會自動調用基礎類默認構建器。在第二個構建器中,通過使用super關鍵字,明確調用了帶有一個String參數的基礎類構建器。

例6 本例程演示建立和應用自己的異常類。

///
//: Inheriting.java

// Inheriting your own exceptions
class MyException extends Exception {
  public MyException() {}
  public MyException(String msg) {
    super(msg);
  }
}
publicclass Inheriting {
  publicstaticvoid f() throws MyException {
    System.out.println(
      "Throwing MyException from f()");
    thrownew MyException();
  }
  publicstaticvoid g() throws MyException {
    System.out.println(
      "Throwing MyException from g()");
    thrownew MyException("Originated in g()");
  }
  publicstaticvoid main(String[] args) {
    try {
      f();
    } catch(MyException e) {
      e.printStackTrace();
    }
    try {
      g();
    } catch(MyException e) {
      e.printStackTrace();
    }
  }
}
/// 

輸出結果:
Throwing MyException from f()
MyException
at Inheriting.f(Inheriting.java:14)
at Inheriting.main(Inheriting.java:22)
Throwing MyException from g()
MyException: Originated in g()
at Inheriting.g(Inheriting.java:18)
at Inheriting.main(Inheriting.java:27)

創建自己的異常時,還可以采取更多的操作。我們可添加額外的構建器及成員:

class MyException2 extends Exception {
  public MyException2() {}
  public MyException2(String msg) {
    super(msg);
  }
  public MyException2(String msg, int x) {
    super(msg);
    i = x;
  }
  public int val() { return i; }
  private int i;
}

本章小結:

  1. 應用異常控制機制進行異常處理的格式是
    try{要監控的代碼} 
    catch(XXXException e) {異常處理代碼} 
    finally {必須執行的代碼} 
  2. 不知道有什么異常類好用時可查閱相關包中有哪些XXXException.class文件。而用Exception可捕獲任何異常。 
  3. 在方法聲明中使用了throws關鍵字的必須進行異常控制,否則會報編譯錯誤。 
  4. 也可以創建自己的異常類。