首先看一下異常處理的完整語法,如下:
try{
//(嘗試運行的)程序代碼
}catch(異常類型 異常的變量名){
//異常處理代碼
}finally{
//異常發生,方法返回之前,總是要執行的代碼
}
在Java中,應用try-catch-finally結構可以使我們在出現異常的時候能保證相關資源被按時正確的清理。
我們都知道一個try-catch-finally結構,只要try塊開始執行了,finally塊裏面的代碼保證執行一次並且只執行一次。try-catch-finally給我們提供了在拋出異常時“保證執行某個動作”的機會,機會是提供了,但是finally一定會把握這次機會,完成某個動作的執行嗎?或者說finally能完成它{...}裏的相關操作嗎?(當然這裏除了system.exit()操作,因爲在system.exit()時,finally是不會執行的)
下面我們就通過一個例子來說明一下,一個最常見的實例應用就是jdbc的Connection, Statement, ResultSet的使用。
先看一個例子:
例1:
void test(){
Connection conn = ...;
Statement stmt = ...;
ResultSet rset = ...;
rset.close();
stmt.close();
conn.close();
...
}
乍一看,這個程序沒有問題,按jdbc標準ResultSet, Statement, Connection都close()掉了,不用GC來幫忙回收資源了。但是如果在rset或者stmt關閉時,出現了異常,那麼conn的close()就不能執行了,那麼就會存在着浪費資源的問題。也就是說這個例子這樣寫是有問題的。
這個時候我們就會想到用finally來執行ResultSet, Statement, Connection的close()操作,在finally中,肯定能執行這些close()操作。
例2:
void test(){
Connection conn = null;
Statement stmt = null;
ResultSet rset = null;
try{
conn = ...;
stmt = ...;
rset = ...;
...
} catch(Exception e){
e.printStackTrace();
} finally{
if(rset!=null)rset.close();
if(stmt!=null)stmt.close();
if(conn!=null)conn.close();
}
}
有人認爲這下終於安全了,不會存在資源浪費的情況了。
其實很多人都是這麼認爲的,但是在finally裏close的時候,也是會拋出異常的。如果在執行if(stmt!=null)stmt.close(); 出現了異常,那麼conn.close()是不會執行的,finally{}不能完全執行結束。這樣寫還是有可能出現問題的。
所以就需要對上面的例2再進行改造:
例3:
void test(){
Connection conn = null;
Statement stmt = null;
ResultSet rset = null;
try{
conn = ...;
stmt = ...;
rset = ...;
...
}
finally{
try{
if(rset!=null)rset.close();
}catch(Exception e){
e.printStackTrace();
}
try{
if(stmt!=null)stmt.close();
}catch(Exception e){
e.printStackTrace();
}
try{
if(conn!=null)conn.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
這樣就可以解決因爲異常而存在浪費資源的問題了。一般的Exception通過例3都能解決這些問題。
但是除了Exception,我們的程序還會碰到Error,那麼上面這個例子是否也能處理Error呢?衆所周知,Error代表不可恢復錯誤,是程序無法處理的錯誤,所以我們也處理不了這種Error。所以在finally裏執行stmt.close()時,如果出現Error,那麼conn.close()還是不會被執行的。finally還是不能執完全行結束。那我們遇到Error時,該怎麼處理裏?
這裏我們可以把Error轉成Exception或者RuntimeException,然後拋出,再做處理。因爲對於一個應用系統來說,系統所發生的任何異常或者錯誤對操作用戶來說都是系統"運行時"異常,都是這個應用系統內部的異常。這也是將Error轉成Exception或者RuntimeException的指導原則(當然也可以將Exception-->RuntimeException)。
①:Error到Exception:將錯誤轉換爲異常,並繼續拋出。例如Spring WEB框架中,將org.springframework.web.servlet.DispatcherServlet的doDispatch()方法中,將捕獲的錯誤轉譯爲一個NestedServletException異常。這樣做的目的是爲了最大限度挽回因錯誤發生帶來的負面影響。因爲一個Error常常是很嚴重的錯誤,可能會引起系統掛起。
②:Exception到RuntimeException:將檢查異常轉換爲RuntimeException可以讓程序代碼變得更優雅,讓開發人員集中經理設計更合理的程序代碼,反過來也增加了系統發生異常的可能性。
③:Error到RuntimeException:目的還是一樣的。把所有的異常和錯誤轉譯爲不檢查異常,這樣可以讓代碼更爲簡潔,還有利於對錯誤和異常信息的統一處理。
所以例3總的來說,還是存在着一些問題,這樣對於上面的例3我們又需要進行程序的修改(在例4中就不介紹Error與Exception或者RuntimeException的轉換了):
例4:
void test(){
final Connection conn = ...;
try{
final Statement stmt = ...;
try{
final ResultSet rset = ...;
try{
...
}catch(Exception e){
e.printStackTrace();
}finally{rset.close();}
}catch(Exception e){
...
}finally{stmt.close();}
}catch(Exception e){
...
}finally{conn.close();}
}
這樣每建立一個需要清理的資源,就用一個try-catch-finally來保證它可以被清理掉。當然了,在執行close操作的時候,我們必須要注意close的次序。通過例4你會發現,close的次序問題好像已經被解決了,不用去考慮這個問題了。
=====================================================================================================
在Java裏finally子句的處理還有其他詭異的地方。比如:它可以改寫返回值。
如下所示:
public class Test {
public static void main(String[] args) {
System.out.println(ret(-1));
System.out.println(ret(1));
}
private static boolean ret(int i) {
try {
if (i < 0) {
return false;
} else {
return true;
}
}catch (Exception e) {
// TODO: handle exception
}finally{
return false;
}
}
}
兩次的返回結果都是:false。其實在調用ret(1)的時候,按道理應該返回true,但是就因爲finally裏的return操作改寫了返回值。所以一般情況下,finally裏不會return。
=====================================================================================================
其實在例4中還是存在着一些小問題的:
void test(){
final Connection conn = ...;
try{
final Statement stmt = ...;
try{
final ResultSet rset = ...;
try{④
...③
}catch(Exception e){①
e.printStackTrace();②
}finally{
rset.close();⑤
}
}catch(Exception e){
...
}finally{stmt.close();}
}catch(Exception e){
...
}finally{conn.close();}
}
①: 這裏應該指定具體的異常,而不提倡用一個catch(Exception e)語句捕獲所有的異常。
catch語句表示我們預期會出現某種異常,而且希望能夠處理該異常。異常類的作用就是告訴Java編譯器我們想要處理的是哪一種異常。
在這段程序中,最明顯的一個就是SQLException,這是JDBC操作中常見的異常。所以我們在捕獲這個異常的時候最好能說明具體是那個異常。
如果這try塊中有多個異常產生,那麼最好是通過多個catch來捕獲不同的異常,而不是爲了圖省事,就用一個catch(Exception e)語句捕獲所有的異常。
②:這裏異常丟棄了,因爲調用一下printStackTrace算不上“處理異常”,調用printStackTrace對調試程序有幫助,但程序調試階段結束之後,printStackTrace就不應再在異常處理模塊中擔負主要責任了。
所以在異常處理的時候針對具體異常採取具體行動,例如修正問題、提醒某個人或進行其他一些處理,要根據具體的情形確定應該採取的動作。 認爲自己不能處理的一場,可以重新拋出異常也不失爲一種選擇,或者把該異常轉換成另一種異常(可以把一個低級的異常轉換成應用級的異常)。
③:如果在這個地方進行數據循環輸出,而在循環輸出時拋出了異常,那麼循環的執行就會被打斷的,這樣就會導致用戶將收到一份不完整的(或者錯誤的)數據,但卻得不到任何有關這份數據是否完整的提示。對於有些系統來說,數據不完整可能比系統停止運行帶來更大的損失。
這個時候就要在處理異常的時候向輸出設備寫一些信息,聲明數據的不完整性;另一種可能有效的辦法是,先緩衝要輸出的數據,準備好全部數據之後再一次性輸出,如下所示:
catch(SQLException sqlex)
{
out.println("警告:數據不完整");
throw new SQLException("讀取數據時出現SQL錯誤", sqlex);
}
④:try塊不要過於龐大
把大量的語句裝入單個巨大的try塊就象是把所有東西就扔到一個大箱子,不能是吃的,還是用的,雖然東西是帶上了,但要找出來卻不容易。
⑤:其實應該跟例3一樣
finally{
try{
if(rset!=null)rset.close();
}catch(Exception e){
e.printStackTrace();
}
}