文章來源:
作者:Maverick
blog:http://blog.csdn.net/zhaohuabing
1 引言
在JAVA語言出現(xiàn)以前,傳統(tǒng)的異常處理方式多采用返回值來標(biāo)識(shí)程序出現(xiàn)的異常情況,這種方式雖然為程序員所熟悉,但卻有多個(gè)壞處。首先,一個(gè)API可以返回任意的返回值,而這些返回值本身并不能解釋該返回值是否代表一個(gè)異常情況發(fā)生了和該異常的具體情況,需要調(diào)用API的程序自己判斷并解釋返回值的含義。其次,并沒有一種機(jī)制來保證異常情況一定會(huì)得到處理,調(diào)用程序可以簡單的忽略該返回值,需要調(diào)用API的程序員記住去檢測返回值并處理異常情況。這種方式還讓程序代碼變得晦澀冗長,當(dāng)進(jìn)行IO操作等容易出現(xiàn)異常情況的處理時(shí),你會(huì)發(fā)現(xiàn)代碼的很大部分用于處理異常情況的switch分支,程序代碼的可讀性變得很差。
上面提到的問題,JAVA的異常處理機(jī)制提供了很好的解決方案。通過拋出JDK預(yù)定義或者自定義的異常,能夠表明程序中出現(xiàn)了什么樣的異常情況;而且JAVA的語言機(jī)制保證了異常一定會(huì)得到恰當(dāng)?shù)奶幚恚缓侠淼氖褂卯惓L幚頇C(jī)制,會(huì)讓程序代碼清晰易懂。
2 JAVA異常的處理機(jī)制
當(dāng)程序中拋出一個(gè)異常后,程序從程序中導(dǎo)致異常的代碼處跳出,java虛擬機(jī)檢測尋找和try關(guān)鍵字匹配的處理該異常的catch塊,如果找到,將控制權(quán)交到catch塊中的代碼,然后繼續(xù)往下執(zhí)行程序,try塊中發(fā)生異常的代碼不會(huì)被重新執(zhí)行。如果沒有找到處理該異常的catch塊,在所有的finally塊代碼被執(zhí)行和當(dāng)前線程的所屬的ThreadGroup的uncaughtException方法被調(diào)用后,遇到異常的當(dāng)前線程被中止。
3 JAVA異常的類層次
JAVA異常的類層次如下圖所示:

圖1 JAVA異常的類層次
Throwable是所有異常的基類,程序中一般不會(huì)直接拋出Throwable對(duì)象,Exception和Error是Throwable的子類,Exception下面又有RuntimeException和一般的Exception兩類。可以把JAVA異常分為三類:
第一類是Error,Error表示程序在運(yùn)行期間出現(xiàn)了十分嚴(yán)重、不可恢復(fù)的錯(cuò)誤,在這種情況下應(yīng)用程序只能中止運(yùn)行,例如JAVA 虛擬機(jī)出現(xiàn)錯(cuò)誤。Error是一種unchecked Exception,編譯器不會(huì)檢查Error是否被處理,在程序中不用捕獲Error類型的異常;一般情況下,在程序中也不應(yīng)該拋出Error類型的異常。
第二類是RuntimeException, RuntimeException 是一種unchecked Exception,即表示編譯器不會(huì)檢查程序是否對(duì)RuntimeException作了處理,在程序中不必捕獲RuntimException類型的異常,也不必在方法體聲明拋出RuntimeException類。RuntimeException發(fā)生的時(shí)候,表示程序中出現(xiàn)了編程錯(cuò)誤,所以應(yīng)該找出錯(cuò)誤修改程序,而不是去捕獲RuntimeException。
第三類是一般的checked Exception,這也是在編程中使用最多的Exception,所有繼承自Exception并且不是RuntimeException的異常都是checked Exception,如圖1中的IOException和ClassNotFoundException。JAVA 語言規(guī)定必須對(duì)checked Exception作處理,編譯器會(huì)對(duì)此作檢查,要么在方法體中聲明拋出checked Exception,要么使用catch語句捕獲checked Exception進(jìn)行處理,不然不能通過編譯。checked Exception用于以下的語義環(huán)境:
(1) 該異常發(fā)生后是可以被恢復(fù)的,如一個(gè)Internet連接發(fā)生異常被中止后,可以重新連接再進(jìn)行后續(xù)操作。
(2) 程序依賴于不可靠的外部條件,該依賴條件可能出錯(cuò),如系統(tǒng)IO。
(3) 該異常發(fā)生后并不會(huì)導(dǎo)致程序處理錯(cuò)誤,進(jìn)行一些處理后可以繼續(xù)后續(xù)操作。
4 JAVA異常處理中的注意事項(xiàng)
合理使用JAVA異常機(jī)制可以使程序健壯而清晰,但不幸的是,JAVA異常處理機(jī)制常常被錯(cuò)誤的使用,下面就是一些關(guān)于Exception的注意事項(xiàng):
1. 不要忽略checked Exception
請(qǐng)看下面的代碼:
try
{
method1(); //method1拋出ExceptionA
}
catch(ExceptionA e)
{
e.printStackTrace();
}
上面的代碼似乎沒有什么問題,捕獲異常后將異常打印,然后繼續(xù)執(zhí)行。事實(shí)上在catch塊中對(duì)發(fā)生的異常情況并沒有作任何處理(打印異常不能是算是處理異常,因?yàn)樵诔绦蚪桓哆\(yùn)行后調(diào)試信息就沒有什么用處了)。這樣程序雖然能夠繼續(xù)執(zhí)行,但是由于這里的操作已經(jīng)發(fā)生異常,將會(huì)導(dǎo)致以后的操作并不能按照預(yù)期的情況發(fā)展下去,可能導(dǎo)致兩個(gè)結(jié)果:
一是由于這里的異常導(dǎo)致在程序中別的地方拋出一個(gè)異常,這種情況會(huì)使程序員在調(diào)試時(shí)感到迷惑,因?yàn)樾碌漠惓伋龅牡胤讲⒉皇浅绦蛘嬲l(fā)生問題的地方,也不是發(fā)生問題的真正原因;
另外一個(gè)是程序繼續(xù)運(yùn)行,并得出一個(gè)錯(cuò)誤的輸出結(jié)果,這種問題更加難以捕捉,因?yàn)楹芸赡馨阉?dāng)成一個(gè)正確的輸出。
那么應(yīng)該如何處理呢,這里有四個(gè)選擇:
(1) 處理異常,進(jìn)行修復(fù)以讓程序繼續(xù)執(zhí)行。
(2) 重新拋出異常,在對(duì)異常進(jìn)行分析后發(fā)現(xiàn)這里不能處理它,那么重新拋出異常,讓調(diào)用者處理。
(3) 將異常轉(zhuǎn)換為用戶可以理解的自定義異常再拋出,這時(shí)應(yīng)該注意不要丟失原始異常信息(見5)。
(4) 不要捕獲異常。
因此,當(dāng)捕獲一個(gè)unchecked Exception的時(shí)候,必須對(duì)異常進(jìn)行處理;如果認(rèn)為不必要在這里作處理,就不要捕獲該異常,在方法體中聲明方法拋出異常,由上層調(diào)用者來處理該異常。
2. 不要一次捕獲所有的異常
請(qǐng)看下面的代碼:
try
{
method1(); //method1拋出ExceptionA
method2(); //method1拋出ExceptionB
method3(); //method1拋出ExceptionC
}
catch(Exception e)
{
……
}
這是一個(gè)很誘人的方案,代碼中使用一個(gè)catch子句捕獲了所有異常,看上去完美而且簡潔,事實(shí)上很多代碼也是這樣寫的。但這里有兩個(gè)潛在的缺陷,一是針對(duì)try塊中拋出的每種Exception,很可能需要不同的處理和恢復(fù)措施,而由于這里只有一個(gè)catch塊,分別處理就不能實(shí)現(xiàn)。二是try塊中還可能拋出RuntimeException,代碼中捕獲了所有可能拋出的RuntimeException而沒有作任何處理,掩蓋了編程的錯(cuò)誤,會(huì)導(dǎo)致程序難以調(diào)試。
下面是改正后的正確代碼:
try
{
method1(); //method1拋出ExceptionA
method2(); //method1拋出ExceptionB
method3(); //method1拋出ExceptionC
}
catch(ExceptionA e)
{
……
}
catch(ExceptionB e)
{
……
}
catch(ExceptionC e)
{
……
}
3. 使用finally塊釋放資源
finally關(guān)鍵字保證無論程序使用任何方式離開try塊,finally中的語句都會(huì)被執(zhí)行。在以下三種情況下會(huì)進(jìn)入finally塊:
(1) try塊中的代碼正常執(zhí)行完畢。
(2) 在try塊中拋出異常。
(3) 在try塊中執(zhí)行return、break、continue。
因此,當(dāng)你需要一個(gè)地方來執(zhí)行在任何情況下都必須執(zhí)行的代碼時(shí),就可以將這些
代碼放入finally塊中。當(dāng)你的程序中使用了外界資源,如數(shù)據(jù)庫連接,文件等,必須將釋放這些資源的代碼寫入finally塊中。
必須注意的是,在finally塊中不能拋出異常。JAVA異常處理機(jī)制保證無論在任何情況下必須先執(zhí)行finally塊然后在離開try塊,因此在try塊中發(fā)生異常的時(shí)候,JAVA虛擬機(jī)先轉(zhuǎn)到finally塊執(zhí)行finally塊中的代碼,finally塊執(zhí)行完畢后,再向外拋出異常。如果在finally塊中拋出異常,try塊捕捉的異常就不能拋出,外部捕捉到的異常就是finally塊中的異常信息,而try塊中發(fā)生的真正的異常堆棧信息則丟失了。
請(qǐng)看下面的代碼:
Connection con = null;
try
{
con = dataSource.getConnection();
……
}
catch(SQLException e)
{
……
throw e;//進(jìn)行一些處理后再將數(shù)據(jù)庫異常拋出給調(diào)用者處理
}
finally
{
try
{
con.close();
}
catch(SQLException e)
{
e.printStackTrace();
……
}
}
運(yùn)行程序后,調(diào)用者得到的信息如下
java.lang.NullPointerException
at myPackage.MyClass.method1(methodl.java:266)
而不是我們期望得到的數(shù)據(jù)庫異常。這是因?yàn)檫@里的con是null的關(guān)系,在finally語句中拋出了NullPointerException,在finally塊中增加對(duì)con是否為null的判斷可以避免產(chǎn)生這種情況。
4. 異常不能影響對(duì)象的狀態(tài)
異常產(chǎn)生后不能影響對(duì)象的狀態(tài),這是異常處理中的一條重要規(guī)則。 在一個(gè)函數(shù)
中發(fā)生異常后,對(duì)象的狀態(tài)應(yīng)該和調(diào)用這個(gè)函數(shù)之前保持一致,以確保對(duì)象處于正確的狀態(tài)中。
如果對(duì)象是不可變對(duì)象(不可變對(duì)象指調(diào)用構(gòu)造函數(shù)創(chuàng)建后就不能改變的對(duì)象,即
創(chuàng)建后沒有任何方法可以改變對(duì)象的狀態(tài)),那么異常發(fā)生后對(duì)象狀態(tài)肯定不會(huì)改變。如果是可變對(duì)象,必須在編程中注意保證異常不會(huì)影響對(duì)象狀態(tài)。有三個(gè)方法可以達(dá)到這個(gè)目的:
(1) 將可能產(chǎn)生異常的代碼和改變對(duì)象狀態(tài)的代碼分開,先執(zhí)行可能產(chǎn)生異常的代碼,如果產(chǎn)生異常,就不執(zhí)行改變對(duì)象狀態(tài)的代碼。
(2) 對(duì)不容易分離產(chǎn)生異常代碼和改變對(duì)象狀態(tài)代碼的方法,定義一個(gè)recover方法,在異常產(chǎn)生后調(diào)用recover方法修復(fù)被改變的類變量,恢復(fù)方法調(diào)用前的類狀態(tài)。
(3) 在方法中使用對(duì)象的拷貝,這樣當(dāng)異常發(fā)生后,被影響的只是拷貝,對(duì)象本身不會(huì)受到影響。
5. 丟失的異常
請(qǐng)看下面的代碼:
public void method2()
{
try
{
……
method1(); //method1進(jìn)行了數(shù)據(jù)庫操作
}
catch(SQLException e)
{
……
throw new MyException(“發(fā)生了數(shù)據(jù)庫異常:”+e.getMessage);
}
}
public void method3()
{
try
{
method2();
}
catch(MyException e)
{
e.printStackTrace();
……
}
}
上面method2的代碼中,try塊捕獲method1拋出的數(shù)據(jù)庫異常SQLException后,拋出了新的自定義異常MyException。這段代碼是否并沒有什么問題,但看一下控制臺(tái)的輸出:
MyException:發(fā)生了數(shù)據(jù)庫異常:對(duì)象名稱 'MyTable' 無效。
at MyClass.method2(MyClass.java:232)
at MyClass.method3(MyClass.java:255)
原始異常SQLException的信息丟失了,這里只能看到method2里面定義的MyException的堆棧情況;而method1中發(fā)生的數(shù)據(jù)庫異常的堆棧則看不到,如何排錯(cuò)呢,只有在method1的代碼行中一行行去尋找數(shù)據(jù)庫操作語句了,祈禱method1的方法體短一些吧。
JDK的開發(fā)者們也意識(shí)到了這個(gè)情況,在JDK1.4.1中,Throwable類增加了兩個(gè)構(gòu)造方法,public Throwable(Throwable cause)和public Throwable(String message,Throwable cause),在構(gòu)造函數(shù)中傳入的原始異常堆棧信息將會(huì)在printStackTrace方法中打印出來。但對(duì)于還在使用JDK1.3的程序員,就只能自己實(shí)現(xiàn)打印原始異常堆棧信息的功能了。實(shí)現(xiàn)過程也很簡單,只需要在自定義的異常類中增加一個(gè)原始異常字段,在構(gòu)造函數(shù)中傳入原始異常,然后重載printStackTrace方法,首先調(diào)用類中保存的原始異常的printStackTrace方法,然后再調(diào)用super.printStackTrace方法就可以打印出原始異常信息了。可以這樣定義前面代碼中出現(xiàn)的MyException類:
public class MyExceptionextends Exception
{
//構(gòu)造函數(shù)
public SMException(Throwable cause)
{
this.cause_ = cause;
}
public MyException(String s,Throwable cause)
{
super(s);
this.cause_ = cause;
}
//重載printStackTrace方法,打印出原始異常堆棧信息
public void printStackTrace()
{
if (cause_ != null)
{
cause_.printStackTrace();
}
super.printStackTrace(s);
}
public void printStackTrace(PrintStream s)
{
if (cause_ != null)
{
cause_.printStackTrace(s);
}
super.printStackTrace(s);
}
public void printStackTrace(PrintWriter s)
{
if (cause_ != null)
{
cause_.printStackTrace(s);
}
super.printStackTrace(s);
}
//原始異常
private Throwable cause_;
}
6. 不要使用同時(shí)使用異常機(jī)制和返回值來進(jìn)行異常處理
下面是我們項(xiàng)目中的一段代碼
try
{
doSomething();
}
catch(MyException e)
{
if(e.getErrcode == -1)
{
……
}
if(e.getErrcode == -2)
{
……
}
……
}
假如在過一段時(shí)間后來看這段代碼,你能弄明白是什么意思嗎?混合使用JAVA異常處理機(jī)制和返回值使程序的異常處理部分變得“丑陋不堪”,并難以理解。如果有多種不同的異常情況,就定義多種不同的異常,而不要像上面代碼那樣綜合使用Exception和返回值。
修改后的正確代碼如下:
try
{
doSomething(); //拋出MyExceptionA和MyExceptionB
}
catch(MyExceptionA e)
{
……
}
catch(MyExceptionB e)
{
……
}
7. 不要讓try塊過于龐大
出于省事的目的,很多人習(xí)慣于用一個(gè)龐大的try塊包含所有可能產(chǎn)生異常的代碼,
這樣有兩個(gè)壞處:
閱讀代碼的時(shí)候,在try塊冗長的代碼中,不容易知道到底是哪些代碼會(huì)拋出哪些異常,不利于代碼維護(hù)。
使用try捕獲異常是以程序執(zhí)行效率為代價(jià)的,將不需要捕獲異常的代碼包含在try塊中,影響了代碼執(zhí)行的效率。