1 Java的異常控制機(jī)制

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

即使不使用try-catch結(jié)構(gòu),發(fā)生異常時(shí)Java的異常控制機(jī)制也會(huì)捕獲該異常,輸出異常的名稱并從異常發(fā)生的位置打印一個(gè)堆棧跟蹤。然后立即終止程序的運(yùn)行。下面的例子發(fā)生了一個(gè)“零除”異常,后面的hello沒有被打印。

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

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

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

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

///
// 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!" );
 
}
}
///

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

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

2 異常類、異常規(guī)范和throw語句

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

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

在其他包中也有相關(guān)的異常類,例如io包中有IOEception類。利用異常的命名規(guī)則,你可以使用下面的DOS命令在包所在的目錄查看有什么異常類可用:
    DIR *Eception.class 
對于運(yùn)行時(shí)異常RuntimeException,我們沒必要專門為它寫一個(gè)異常控制器,因?yàn)樗鼈兪怯捎诰幊滩粐?yán)謹(jǐn)而造成的邏輯錯(cuò)誤。只要讓出現(xiàn)終止,它會(huì)自動(dòng)得到處理。需要程序員進(jìn)行異常處理的是那些非運(yùn)行期異常。
Throwable有三個(gè)基本方法:

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

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

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

因?yàn)镋xception類是一切異常的根,所以對任何一個(gè)現(xiàn)有的異常類都可以使用上述方法。

異常規(guī)范 throws

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

例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

在一個(gè)try區(qū)中潛在的異常可能是多種類型的,那時(shí)我們需要用多個(gè)catch塊來捕獲和處理這些異常。但異常發(fā)生時(shí)擲出了某類異常對象,Java依次逐個(gè)檢查這些異常控制器,發(fā)現(xiàn)與擲出的異常類型匹配時(shí)就執(zhí)行那以段處理代碼,而其余的不會(huì)被執(zhí)行。為了防止可能遺漏了某一類異常控制器,可以放置一個(gè)捕獲Exception類的控制器。Exception是可以從任何類方法中“擲”出的基本類型。但是它必須放在最后一個(gè)位置,因?yàn)樗軌蚪孬@任何異常,從而使后面具體的異常控制器不起作用。下面的示例說明了這一點(diǎn)。

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

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

運(yùn)行結(jié)果:
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清理

我們經(jīng)常會(huì)遇到這樣的情況,無論一個(gè)異常是否發(fā)生,必須執(zhí)行某些特定的代碼。比如文件已經(jīng)打開,關(guān)閉文件是必須的。但是,在try區(qū)內(nèi)位于異常發(fā)生點(diǎn)以后的代碼,在發(fā)生異常后不會(huì)被執(zhí)行。在catch區(qū)中的代碼在異常沒有發(fā)生的情況下不會(huì)被執(zhí)行。為了無論異常是否發(fā)生都要執(zhí)行的代碼,可在所有異常控制器的末尾使用一個(gè)finally從句,在finally塊中放置這些代碼。(但在恢復(fù)內(nèi)存時(shí)一般都不需要,因?yàn)槔占鲿?huì)自動(dòng)照料一切。)所以完整的異常控制結(jié)構(gòu)象下面這個(gè)樣子:

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

例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"
      }
    }
  }
}
///

運(yùn)行結(jié)果:
Exception thrown
in finally clause
No exception
in finally clause
一開始count=0發(fā)生異常,然后進(jìn)入finally塊;進(jìn)入循環(huán)第二輪沒有異常,但又執(zhí)行一次finally塊,并在其中跳出循環(huán)。
下面我們給出一個(gè)有的實(shí)用但較為復(fù)雜一點(diǎn)的程序。我們創(chuàng)建了一個(gè)InputFile的類。它的作用是打開一個(gè)文件,然后每次讀取它的一行內(nèi)容。

例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();
    }
  }
}
///

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

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

4 創(chuàng)建自己的異常類

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

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

例6 本例程演示建立和應(yīng)用自己的異常類。

///
//: 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();
    }
  }
}
/// 

輸出結(jié)果:
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)

創(chuàng)建自己的異常時(shí),還可以采取更多的操作。我們可添加額外的構(gòu)建器及成員:

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

本章小結(jié):

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