看板 ott
作者 標題 JAVA異常機制介紹/如何正確的進行JAVA異常處理
時間 2010年02月13日 Sat. AM 10:57:17
![[圖]](http://p.blog.csdn.net/images/p_blog_csdn_net/zhaohuabing/201680/o_JAVA%e5%bc%82%e5%b8%b8%e6%9c%ba%e5%88%b6%e4%bb%8b%e7%bb%8d%e5%a6%82%e4%bd%95%e6%ad%a3%e7%a1%ae%e7%9a%84%e8%bf%9b%e8%a1%8cJAVA%e5%bc%82%e5%b8%b8%e5%a4%84%e7%90%86_%e5%9b%be1.gif)
作者:Maverick
blog:http://blog.csdn.net/zhaohuabing 轉載請註明出處
1 引言
在JAVA語言出現以前,傳統的異常處理方式多採用返回值來標識程序出現的
異常情況,這種方式雖然為程序員所熟悉,但卻有多個壞處。首先,一個AP
I可以返回任意的返回值,而這些返回值本身並不能解釋該返回值是否代表
一個異常情況發生了和該異常的具體情況,需要調用API的程序自己判斷並
解釋返回值的含義。其次,並沒有一種機制來保證異常情況一定會得到處理
,調用程序可以簡單的忽略該返回值,需要調用API的程序員記住去檢測返
回值並處理異常情況。這種方式還讓程序代碼變得晦澀冗長,當進行IO操作
等容易出現異常情況的處理時,你會發現代碼的很大部分用於處理異常情況
的switch分支,程序代碼的可讀性變得很差。
上面提到的問題,JAVA的異常處理機制提供了很好的解決方案。通過拋出JD
K預定義或者自定義的異常,能夠表明程序中出現了什麼樣的異常情況;而且
JAVA的語言機制保證了異常一定會得到恰當的處理;合理的使用異常處理機
制,會讓程序代碼清晰易懂。
2 JAVA異常的處理機制
當程序中拋出一個異常後,程序從程序中導致異常的代碼處跳出,java虛擬
機檢測尋找和try關鍵字匹配的處理該異常的catch塊,如果找到,將控制權
交到catch塊中的代碼,然後繼續往下執行程序,try塊中發生異常的代碼不
會被重新執行。如果沒有找到處理該異常的catch塊,在所有的finally塊代
碼被執行和當前線程的所屬的ThreadGroup的uncaughtException方法被調用
後,遇到異常的當前線程被中止。
3 JAVA異常的類層次
JAVA異常的類層次如下圖所示:
圖1 JAVA異常的類層次
Throwable是所有異常的基類,程序中一般不會直接拋出Throwable對象,Ex
ception和Error是Throwable的子類,Exception下面又有RuntimeException
和一般的Exception兩類。可以把JAVA異常分為三類:
第一類是Error,Error表示程序在運行期間出現了十分嚴重、不可恢復的錯
誤,在這種情況下應用程序只能中止運行,例如JAVA
虛擬機出現錯誤。Error是一種unchecked
Exception,編譯器不會檢查Error是否被處理,在程序中不用捕獲Error類型
的異常;一般情況下,在程序中也不應該拋出Error類型的異常。
第二類是RuntimeException, RuntimeException 是一種
unchecked Exception,即表示編譯器不會檢查程序是否對RuntimeException
作了處理,在程序中不必捕獲RuntimException類型的異常,也不必在方法體
聲明拋出RuntimeException類。RuntimeException發生的時候,表示程序中
出現了編程錯誤,所以應該找出錯誤修改程序,
而不是去捕獲RuntimeException。
第三類是一般的checked Exception,這也是在編程中使用最多的
Exception,所有繼承自Exception並且不是RuntimeException的異常都是
checkedException,如圖1中的IOException和ClassNotFoundException。
JAVA語言規定必須對checked
Exception作處理,編譯器會對此作檢查,要麼在方法體中聲明拋出checked
Exception,要麼使用catch語句捕獲checked
Exception進行處理,不然不能通過編譯。checked
Exception用於以下的語義環境:
(1)
該異常發生後是可以被恢復的,如一個Internet連接發生異常被中止後,可
以重新連接再進行後續操作。
(2) 程序依賴於不可靠的外部條件,該依賴條件可能出錯,如系統IO。
(3)
該異常發生後並不會導致程序處理錯誤,進行一些處理後可以繼續後續操作
。
4 JAVA異常處理中的注意事項
合理使用JAVA異常機制可以使程序健壯而清晰,但不幸的是,JAVA異常處理
機制常常被錯誤的使用,下面就是一些關於Exception的注意事項:
1. 不要忽略checked Exception
請看下面的代碼:
try
{
method1(); //method1拋出ExceptionA
}
catch(ExceptionA e)
{
e.printStackTrace();
}
上面的代碼似乎沒有什麼問題,捕獲異常後將異常打印,然後繼續執行。事
實上在catch塊中對發生的異常情況並沒有作任何處理(打印異常不能是算是
處理異常,因為在程序交付運行後調試信息就沒有什麼用處了)。這樣程序
雖然能夠繼續執行,但是由於這裡的操作已經發生異常,將會導致以後的操
作並不能按照預期的情況發展下去,可能導致兩個結果:
一是由於這裡的異常導致在程序中別的地方拋出一個異常,這種情況會使程
序員在調試時感到迷惑,因為新的異常拋出的地方並不是程序真正發生問題
的地方,也不是發生問題的真正原因;
另外一個是程序繼續運行,並得出一個錯誤的輸出結果,這種問題更加難以
捕捉,因為很可能把它當成一個正確的輸出。
那麼應該如何處理呢,這裡有四個選擇:
(1) 處理異常,進行修復以讓程序繼續執行。
(2)
重新拋出異常,在對異常進行分析後發現這裡不能處理它,那麼重新拋出異
常,讓調用者處理。
(3)
將異常轉換為用戶可以理解的自定義異常再拋出,這時應該注意不要丟失原
始異常信息(見5)。
(4) 不要捕獲異常。
因此,當捕獲一個unchecked
Exception的時候,必須對異常進行處理;如果認為不必要在這裡作處理,
就不要捕獲該異常,在方法體中聲明方法拋出異常,由上層調用者來處理該
異常。
2. 不要一次捕獲所有的異常
請看下面的代碼:
try
{
method1(); //method1拋出ExceptionA
method2(); //method1拋出ExceptionB
method3(); //method1拋出ExceptionC
}
catch(Exception e)
{
……
}
這是一個很誘人的方案,代碼中使用一個catch子句捕獲了所有異常,看上
去完美而且簡潔,事實上很多代碼也是這樣寫的。但這裡有兩個潛在的缺陷
,一是針對try塊中拋出的每種Exception,很可能需要不同的處理和恢復措
施,而由於這裡只有一個catch塊,分別處理就不能實現。二是try塊中還可
能拋出RuntimeException,代碼中捕獲了所有可能拋出的RuntimeException
而沒有作任何處理,掩蓋了編程的錯誤,會導致程序難以調試。
下面是改正後的正確代碼:
try
{
method1(); //method1拋出ExceptionA
method2(); //method1拋出ExceptionB
method3(); //method1拋出ExceptionC
}
catch(ExceptionA e)
{
……
}
catch(ExceptionB e)
{
……
}
catch(ExceptionC e)
{
……
}
3. 使用finally塊釋放資源
finally關鍵字保證無論程序使用任何方式離開try塊,finally中的語句都
會被執行。在以下三種情況下會進入finally塊:
(1) try塊中的代碼正常執行完畢。
(2) 在try塊中拋出異常。
(3) 在try塊中執行return、break、continue。
因此,當你需要一個地方來執行在任何情況下都必須執行的代碼時,就可以
將這些代碼放入finally塊中。當你的程序中使用了外界資源,如數據庫連接
,文件等,必須將釋放這些資源的代碼寫入finally塊中。
必須注意的是,在finally塊中不能拋出異常。JAVA異常處理機制保證無論
在任何情況下必須先執行finally塊然後在離開try塊,因此在try塊中發生
異常的時候,JAVA虛擬機先轉到finally塊執行finally塊中的代碼,finall
y塊執行完畢後,再向外拋出異常。如果在finally塊中拋出異常,try塊捕
捉的異常就不能拋出,外部捕捉到的異常就是finally塊中的異常信息,而t
ry塊中發生的真正的異常堆棧信息則丟失了。
請看下面的代碼:
Connection con = null;
try
{
con = dataSource.getConnection();
……
}
catch(SQLException e)
{
……
throw e;//進行一些處理後再將數據庫異常拋出給調用者處理
}
finally
{
try
{
con.close();
}
catch(SQLException e)
{
e.printStackTrace();
……
}
}
運行程序後,調用者得到的信息如下
java.lang.NullPointerException
at myPackage.MyClass.method1(methodl.java:266)
而不是我們期望得到的數據庫異常。這是因為這裡的con是null的關係,在f
inally語句中拋出了NullPointerException,在finally塊中增加對con是否
為null的判斷可以避免產生這種情況。
4. 異常不能影響對象的狀態
異常產生後不能影響對象的狀態,這是異常處理中的一條重要規則。
在一個函數
中發生異常後,對象的狀態應該和調用這個函數之前保持一致,以確保對象
處於正確的狀態中。
如果對象是不可變對象(不可變對象指調用構造函數創建後就不能改變的對
象,即
創建後沒有任何方法可以改變對象的狀態),那麼異常發生後對象狀態肯定
不會改變。如果是可變對象,必須在編程中注意保證異常不會影響對象狀態
。
有三個方法可以達到這個目的:
(1)
將可能產生異常的代碼和改變對象狀態的代碼分開,先執行可能產生異常的
代碼,如果產生異常,就不執行改變對象狀態的代碼。
(2)
對不容易分離產生異常代碼和改變對象狀態代碼的方法,定義一個recover
方法,在異常產生後調用recover方法修復被改變的類變量,恢複方法調用
前的類狀態。
(3)
在方法中使用對象的拷貝,這樣當異常發生後,被影響的只是拷貝,對象本
身不會受到影響。
5. 丟失的異常
請看下面的代碼:
public void method2()
{
try
{
……
method1(); //method1進行了數據庫操作
}
catch(SQLException e)
{
……
throw new MyException(「發生了數據庫異常:」+e.getMessage);
}
}
public void method3()
{
try
{
method2();
}
catch(MyException e)
{
e.printStackTrace();
……
}
}
上面method2的代碼中,try塊捕獲method1拋出的數據庫異常SQLException
後,拋出了新的自定義異常MyException。
這段代碼是否並沒有什麼問題,
但看一下控制台的輸出:
MyException:發生了數據庫異常:對象名稱 'MyTable' 無效。
at MyClass.method2(MyClass.java:232)
at MyClass.method3(MyClass.java:255)
原始異常SQLException的信息丟失了,這裡只能看到method2里面定義的MyE
xception的堆棧情況;而method1中發生的數據庫異常的堆棧則看不到,如
何排錯呢,只有在method1的代碼行中一行行去尋找數據庫操作語句了,祈
禱method1的方法體短一些吧。
JDK的開發者們也意識到了這個情況,在JDK1.4.1中,Throwable類增加了兩
個構造方法,public Throwable(Throwable cause)和public
Throwable(String message,Throwable
cause),在構造函數中傳入的原始異常堆棧信息將會在printStackTrace方
法中打印出來。但對於還在使用JDK1.3的程序員,就只能自己實現打印原始
異常堆棧信息的功能了。實現過程也很簡單,只需要在自定義的異常類中增
加一個原始異常字段,在構造函數中傳入原始異常,然後重載printStackTr
ace方法,首先調用類中保存的原始異常的printStackTrace方法,然後再調
用super.printStackTrace方法就可以打印出原始異常信息了。
可以這樣定義前面代碼中出現的MyException類:
public class MyExceptionextends Exception
{
//構造函數
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. 不要使用同時使用異常機制和返回值來進行異常處理
下面是我們項目中的一段代碼
try
{
doSomething();
}
catch(MyException e)
{
if(e.getErrcode == -1)
{
……
}
if(e.getErrcode == -2)
{
……
}
……
}
假如在過一段時間後來看這段代碼,你能弄明白是什麼意思嗎?混合使用JA
VA異常處理機制和返回值使程序的異常處理部分變得「醜陋不堪」,並難以
理解。如果有多種不同的異常情況,就定義多種不同的異常,而不要像上面
代碼那樣綜合使用Exception和返回值。
修改後的正確代碼如下:
try
{
doSomething(); //拋出MyExceptionA和MyExceptionB
}
catch(MyExceptionA e)
{
……
}
catch(MyExceptionB e)
{
……
}
7. 不要讓try塊過於龐大
出於省事的目的,很多人習慣於用一個龐大的try塊包含所有可能產生異常
的代碼,
這樣有兩個壞處:
閱讀代碼的時候,在try塊冗長的代碼中,不容易知道到底是哪些代碼會拋
出哪些異常,不利於代碼維護。
使用try捕獲異常是以程序執行效率為代價的,將不需要捕獲異常的代碼包
含在try塊中,影響了代碼執行的效率。
參考資料
[1] Joshua Bloch Effective Java Programming Language Guide
[2] http://java.sun.com/
發表於 @ 2006年05月08日 11:37:00 | 評論( 0 ) | 舉報| 收藏
http://blog.csdn.net/.../archive/2006/05/08/712617.aspx
※ 編輯: ott 時間: 2013-12-15 02:19:15
※ 看板: ott 文章推薦值: 0 目前人氣: 0 累積人氣: 513
回列表(←)
分享