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