Java 語言要求 java 程序中(無論是誰寫的代碼),所有拋出( throw )的異常都必須是從 Throwable 派生而來。 當然,實際的 Java 編程中,由于 JDK 平臺已經為我們設計好了非常豐富和完整的異常對象分類模型。因此, java 程序員一般是不需要再重新定義自己的異常對象。而且即便是需要擴展自定義的異常對象,也往往會從 Exception 派生而來。所以,對于 java 程序員而言,它一般只需要在它的頂級函數中 catch(Exception ex) 就可以捕獲出所有的異常對象。 所有異常對象的根基類是 Throwable , Throwable 從 Object 直接繼承而來(這是 java 系統所強制要求的),并且它實現了 Serializable 接口(這為所有的異常對象都能夠輕松跨越 Java 組件系統做好了最充分的物質準備)。從 Throwable 直接派生出的異常類有 Exception 和 Error 。 Exception 是 java 程序員所最熟悉的,它一般代表了真正實際意義上的異常對象的根基類。也即是說, Exception 和從它派生而來的所有異常都是應用程序能夠 catch 到的,并且可以進行異常錯誤恢復處理的異常類型。而 Error 則表示 Java 系統中出現了一個非常嚴重的異常錯誤,并且這個錯誤可能是應用程序所不能恢復的,例如 LinkageError ,或 ThreadDeath 等。
首先還是看一個例子吧!代碼如下:
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
try
{
BufferedReader rd=null;
Writer wr=null;
try
{
File srcFile = new File((args[0]));
File dstFile = new File((args[1]));
rd = new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), args[2]));
wr = new OutputStreamWriter(new FileOutputStream(dstFile), args[3]);
// 注意下面這條語句,它有什么問題嗎?
if (rd == null || wr == null) throw new Exception("error! test!");
while(true)
{
String sLine = rd.readLine();
if(sLine == null) break;
wr.write(sLine);
wr.write("\r\n");
}
}
finally
{
wr.flush();
wr.close();
rd.close();
}
}
catch(IOException ex)
{
ex.printStackTrace();
}
}
}
熟悉 java 語言的程序員朋友們,你們認為上面的程序有什么問題嗎?編譯能通過嗎?如果不能,那么原因又是為何呢?好了,有了自己的分析和預期之后,不妨親自動手編譯一下上面的小程序,呵呵!結果確實如您所料?是的,的確是編譯時報錯了,錯誤信息如下:
E:\Trans.java:20: unreported exception java.lang.Exception; must be caught or declared to be thrown
if (rd == null || wr == null) throw new Exception("error! test!");
1 error
上面這種編譯錯誤信息,相信 Java 程序員肯定見過(可能還是屢見不鮮!)!
相信老練一些的 Java 程序員一定非常清楚上述編譯出錯的原因。那就是如錯誤信息中(“ must be caught ”)描述的那樣, 在 Java 的異常處理模型中,要求所有被拋出的異常都必須要有對應的“異常處理模塊” 。也即是說,如果你在程序中 throw 出一個異常,那么在你的程序中(函數中)就必須要 catch 這個異常(處理這個異常)。例如上面的例子中,你在第 20 行代碼處,拋出了一個 Exception 類型的異常,但是在該函數中,卻沒有 catch 并處理掉此異常的地方。因此,這樣的程序即便是能夠編譯通過,那么運行時也是致命的(可能導致程序的崩潰),所以, Java 語言干脆在編譯時就盡可能地檢查(并卡住)這種本不應該出現的錯誤,這無疑對提高程序的可靠性大有幫助。
但是,在 Java 語言中,這就是必須的。 如果一個函數中,它運行時可能會向上層調用者函數拋出一個異常,那么,它就必須在該函數的聲明中顯式的注明(采用 throws 關鍵字) 。還記得剛才那條編譯錯誤信息嗎?“ must be caught or declared to be thrown ”,其中“ must be caught ”上面已經解釋了,而后半部分呢?“ declared to be thrown ”是指何意呢?其實指的就是“必須顯式地聲明某個函數可能會向外部拋出一個異常”,也即是說,如果一個函數內部,它可能拋出了一種類型的異常,但該函數內部并不想(或不宜) catch 并處理這種類型的異常,此時,它就必須使用 throws 關鍵字來聲明該函數可能會向外部拋出一個異常,以便于該函數的調用者知曉并能夠及時處理這種類型的異常。下面列出了這幾種情況的比較,代碼如下:
// 示例程序 1 ,這種寫法能夠編譯通過
package com.ginger.exception;
import java.io.*;
public class Trans {
public static void main(String[] args) {
try {
test();
} catch (Exception ex) {
ex.printStackTrace();
}
}
static void test() {
try {
throw new Exception("To show Exception Successed");
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
// 示例程序 2 ,這種寫法就不能夠編譯通過
import java.io.*;
public class Trans {
public static void main(String[] args) {
try {
test();
}
// 雖然這里能夠捕獲到 Exception 類型的異常
catch (Exception ex) {
ex.printStackTrace();
}
}
static void test() {
throw new Exception("test");
}
}
// 示例程序 3 ,這種寫法又能夠被編譯通過
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
try
{
test();
}
catch(Exception ex)
{
ex.printStackTrace();
}
}
// 由于函數聲明了可能拋出 Exception 類型的異常
static void test() throws Exception
{
throw new Exception("test");
}
}
// 示例程序 4 ,它又不能夠被編譯通過了
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
try
{
// 雖然 test() 函數并沒有真正拋出一個 Exception 類型的異常
// 但是由于它函數聲明時,表示它可能拋出一個 Exception 類型的異常
// 所以,這里仍然不能被編譯通過。
// 呵呵!體會到了 Java 異常處理模型的嚴謹吧!
test();
}
catch(IOException ex)
{
ex.printStackTrace();
}
}
static void test() throws Exception
{
}
}
不知上面幾個有聯系的示例是否能夠給大家帶來“豁然開朗”的感覺,坦率的說, Java 提供的異常處理模型并不復雜,相信太多太多 Java 程序員有著比我更深刻的認識。最后,補充一種例外情況,請看如下代碼:
import java.io.*;
public class Trans {
public static void main(String[] args) {
try {
test();
} catch (Exception ex) {
ex.printStackTrace();
}
}
static void test() throws Error {
throw new Error(" 故意拋出一個 Error");
}
}
朋友們!上面的程序能被編譯通過嗎?注意,按照剛才上面所總結出的規律:在 Java 的異常處理模型中,要求所有被拋出的異常都必須要有對應的 catch 塊!那么上面的程序肯定不能被編譯通過,因為 Error 和 Exception 都是從 Throwable 直接派生而來,而 test 函數聲明了它可能拋出 Error 類型的異常,但在 main 函數中卻并沒有 catch(Error) 或 catch(Throwable) 塊,所以它理當是會編譯出錯的!真的嗎?不妨試試!呵呵!結果并非我們之預料,而它恰恰是正確編譯通過了。為何? WHY ? WHY ?
其實,原因很簡單,那就是因為 Error 異常的特殊性。 Java 異常處理模型中規定: Error 和從它派生而來的所有異常,都表示系統中出現了一個非常嚴重的異常錯誤,并且這個錯誤可能是應用程序所不能恢復的(其實這在前面的內容中已提到過)。因此,如果系統中真的出現了一個 Error 類型的異常,那么則表明,系統已處于崩潰不可恢復的狀態中,此時,作為編寫 Java 應用程序的你,已經是沒有必要(也沒能力)來處理此等異常錯誤。所以, javac 編譯器就沒有必要來保證:“在編譯時,所有的 Error 異常都有其對應的錯誤處理模塊”。當然, Error 類型的異常一般都是由系統遇到致命的錯誤時所拋出的,它最后也由 Java 虛擬機所處理。而作為 Java 程序員的你,可能永遠也不會考慮拋出一個 Error 類型的異常。因此 Error 是一個特例情況!
特別關注一下 RuntimeException
上面剛剛討論了一下 Error 類型的異常處理情況, Java 程序員一般無須關注它(處理這種異常)。另外,其實在 Exception 類型的異常對象中,也存在一種比較特別的“異常”類型,那就是 RuntimeException ,雖然它是直接從 Exception 派生而來,但是 Java 編譯器( javac )對 RuntimeException 卻是特殊待遇,而且是照顧有加。不信,看看下面的兩個示例吧!代碼如下:
// 示例程序 1
// 它不能編譯通過,我們可以理解
import java.io.*;
public class Trans {
public static void main(String[] args) {
test();
}
static void test() {
// 注意這條語句
throw new Exception(" 故意拋出一個 Exception");
}
}
// 示例程序 2
// 可它卻為什么能夠編譯通過呢?
import java.io.*;
public class Trans {
public static void main(String[] args) {
test();
}
static void test() {
// 注意這條語句
throw new RuntimeException(" 故意拋出一個 RuntimeException");
}
}
對上面兩個相當類似的程序, javac 編譯時卻遭遇了兩種截然不同的處理,按理說,第 2 個示例程序也應該像第 1 個示例程序那樣,編譯時報錯!但是 javac 編譯它時,卻例外地讓它通過它,而且在運行時, java 虛擬機也捕獲到了這個異常,并且會在 console 打印出詳細的異常信息。運行結果如下:
java.lang.RuntimeException: 故意拋出一個 RuntimeException
at Trans.test(Trans.java:13)
at Trans.main(Trans.java:8)
Exception in thread "main"
為什么對于 RuntimeException 類型的異常(以及從它派生而出的異常類型), javac 和 java 虛擬機都特殊處理呢?要知道,這可是與“ Java 異常處理模型更嚴謹和更安全”的設計原則相抵觸的呀!究竟是為何呢?這簡直讓人不法理解呀!
只不過, Java 語言中, RuntimeException 被統一納入到了 Java 語言和 JDK 的規范之中。請看如下代碼,來驗證一下我們的理解!
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
test();
}
static void test()
{
int i = 4;
int j = 0;
// 運行時,這里將觸發了一個 ArithmeticException
// ArithmeticException 從 RuntimeException 派生而來
System.out.println("i / j = " + i / j);
}
}
運行結果如下:
java.lang.ArithmeticException: / by zero
at Trans.test(Trans.java:16)
at Trans.main(Trans.java:8)
Exception in thread "main"
又如下面的例子,也會產生一個 RuntimeException ,代碼如下:
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
test();
}
static void test()
{
String str = null;
// 運行時,這里將觸發了一個 NullPointerException
// NullPointerException 從 RuntimeException 派生而來
str.compareTo("abc");
}
}
所以,針對 RuntimeException 類型的異常, javac 是無法通過編譯時的靜態語法檢測來判斷到底哪些函數(或哪些區域的代碼)可能拋出這類異常(這完全取決于運行時狀態,或者說運行態所決定的),也正因為如此, Java 異常處理模型中的“ must be caught or declared to be thrown ”規則也不適用于 RuntimeException (所以才有前面所提到過的奇怪編譯現象,這也屬于特殊規則吧)。但是, Java 虛擬機卻需要有效地捕獲并處理此類異常。當然, RuntimeException 也可以被程序員顯式地拋出,而且為了程序的可靠性,對一些可能出現“運行時異常( RuntimeException )”的代碼區域,程序員最好能夠及時地處理這些意外的異常,也即通過 catch(RuntimeExcetion) 或 catch(Exception) 來捕獲它們。如下面的示例程序,代碼如下:
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
try
{
test();
}
// 在上層的調用函數中,最好捕獲所有的 Exception 異常!
catch(Exception e)
{
System.out.println("go here!");
e.printStackTrace();
}
}
// 這里最好顯式地聲明一下,表明該函數可能拋出 RuntimeException
static void test() throws RuntimeException
{
String str = null;
// 運行時,這里將觸發了一個 NullPointerException
// NullPointerException 從 RuntimeException 派生而來
str.compareTo("abc");
}
}