1. 程式人生 > >關於JAVA專案中的常用的異常處理情況總結

關於JAVA專案中的常用的異常處理情況總結

1. JAVA異常處理

在面向過程式的程式語言中,我們可以通過返回值來確定方法是否正常執行。比如在一個c語言編寫的程式中,如果方法正確的執行則返回1.錯誤則返回0。在vb或delphi開發的應用程式中,出現錯誤時,我們就彈出一個訊息框給使用者。

通過方法的返回值我們並不能獲得錯誤的詳細資訊。可能因為方法由不同的程式設計師編寫,當同一類錯誤在不同的方法出現時,返回的結果和錯誤資訊並不一致。

所以java語言採取了一個統一的異常處理機制。

什麼是異常?執行時發生的可被捕獲和處理的錯誤。

在java語言中,Exception是所有異常的父類。任何異常都擴充套件於Exception類。Exception就相當於一個錯誤型別。如果要定義一個新的錯誤型別就擴充套件一個新的Exception子類。採用異常的好處還在於可以精確的定位到導致程式出錯的原始碼位置,並獲得詳細的錯誤資訊。

Java異常處理通過五個關鍵字來實現,try,catch,throw ,throws, finally。具體的異常處理結構由try….catch….finally塊來實現。try塊存放可能出現異常的java語句,catch用來捕獲發生的異常,並對異常進行處理。Finally塊用來清除程式中未釋放的資源。不管理try塊的程式碼如何返回,finally塊都總是被執行。

一個典型的異常處理程式碼

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public String getPassword(String userId) throws DataAccessException{ String sql = “select password from userinfo where userid= '”+userId +”' ”; String password =
null ; Connection con = null ; Statement s = null ; ResultSet rs = null ; try { con = getConnection(); //獲得資料連線 s = con.createStatement(); rs = s.executeQuery(sql); while (rs.next()){ password = rs.getString( 1 ); } rs.close(); s.close(); } Catch(SqlException ex){ throw new DataAccessException(ex); } finally { try { if (con != null ){ con.close(); } } Catch(SQLException sqlEx){ throw new DataAccessException(“關閉連線失敗!”,sqlEx); } } return password; }

可以看出Java的異常處理機制具有的優勢:

給錯誤進行了統一的分類,通過擴充套件Exception類或其子類來實現。從而避免了相同的錯誤可能在不同的方法中具有不同的錯誤資訊。在不同的方法中出現相同的錯誤時,只需要throw 相同的異常物件即可。

獲得更為詳細的錯誤資訊。通過異常類,可以給異常更為詳細,對使用者更為有用的錯誤資訊。以便於使用者進行跟蹤和除錯程式。

把正確的返回結果與錯誤資訊分離。降低了程式的複雜度。呼叫者無需要對返回結果進行更多的瞭解。

強制呼叫者進行異常處理,提高程式的質量。當一個方法宣告需要丟擲一個異常時,那麼呼叫者必須使用try….catch塊對異常進行處理。當然呼叫者也可以讓異常繼續往上一層丟擲。

2. Checked 異常 還是 unChecked 異常?

Java異常分為兩大類:checked 異常和unChecked 異常。所有繼承java.lang.Exception 的異常都屬於checked異常。所有繼承java.lang.RuntimeException的異常都屬於unChecked異常。

當一個方法去呼叫一個可能丟擲checked異常的方法,必須通過try…catch塊對異常進行捕獲進行處理或者重新丟擲。
我們看看Connection介面的createStatement()方法的宣告。

?
1 2 3 4 5 6 7 8 9 10 11 12 public Statement createStatement() throws SQLException; SQLException是checked異常。當呼叫createStatement方法時,java強制呼叫者必須對SQLException進行捕獲處理。 public String getPassword(String userId){ try { …… Statement s = con.createStatement(); …… Catch(SQLException sqlEx){ …… } …… }

或者

?
1 2 3 public String getPassword(String userId) throws SQLException{ Statement s = con.createStatement(); }

(當然,像Connection,Satement這些資源是需要及時關閉的,這裡僅是為了說明checked 異常必須強制呼叫者進行捕獲或繼續丟擲)

unChecked異常也稱為執行時異常,通常RuntimeException都表示使用者無法恢復的異常,如無法獲得資料庫連線,不能開啟檔案等。雖然使用者也可以像處理checked異常一樣捕獲unChecked異常。但是如果呼叫者並沒有去捕獲unChecked異常時,編譯器並不會強制你那麼做。

比如一個把字元轉換為整型數值的程式碼如下:

?
1 2 String str = “ 123 ”; int value = Integer.parseInt(str);

parseInt的方法簽名為:

?
1 public staticint parseInt(String s) throws NumberFormatException

當傳入的引數不能轉換成相應的整數時,將會丟擲NumberFormatException。因為NumberFormatException擴充套件於RuntimeException,是unChecked異常。所以呼叫parseInt方法時無需要try…catch

因為java不強制呼叫者對unChecked異常進行捕獲或往上丟擲。所以程式設計師總是喜歡丟擲unChecked異常。或者當需要一個新的異常類時,總是習慣的從RuntimeException擴充套件。當你去呼叫它些方法時,如果沒有相應的catch塊,編譯器也總是讓你通過,同時你也根本無需要去了解這個方法倒底會丟擲什麼異常。看起來這似乎倒是一個很好的辦法,但是這樣做卻是遠離了java異常處理的真實意圖。並且對呼叫你這個類的程式設計師帶來誤導,因為呼叫者根本不知道需要在什麼情況下處理異常。而checked異常可以明確的告訴呼叫者,呼叫這個類需要處理什麼異常。如果呼叫者不去處理,編譯器都會提示並且是無法編譯通過的。當然怎麼處理是由呼叫者自己去決定的。

所以Java推薦人們在應用程式碼中應該使用checked異常。就像我們在上節提到運用異常的好外在於可以強制呼叫者必須對將會產生的異常進行處理。包括在《java Tutorial》等java官方文件中都把checked異常作為標準用法。

使用checked異常,應意味著有許多的try…catch在你的程式碼中。當在編寫和處理越來越多的try…catch塊之後,許多人終於開始懷疑checked異常倒底是否應該作為標準用法了。

甚至連大名鼎鼎的《thinking in java》的作者Bruce Eckel也改變了他曾經的想法。Bruce Eckel甚至主張把unChecked異常作為標準用法。並發表文章,以試驗checked異常是否應該從java中去掉。Bruce Eckel語:“當少量程式碼時,checked異常無疑是十分優雅的構思,並有助於避免了許多潛在的錯誤。但是經驗表明,對大量程式碼來說結果正好相反”

關於checked異常和unChecked異常的詳細討論可以參考

《java Tutorial》 http://java.sun.com/docs/books/tutorial/essential/exceptions/runtime.html

使用checked異常會帶來許多的問題。

checked異常導致了太多的try…catch 程式碼

可能有很多checked異常對開發人員來說是無法合理地進行處理的,比如SQLException。而開發人員卻不得不去進行try…catch。當開發人員對一個checked異常無法正確的處理時,通常是簡單的把異常打印出來或者是乾脆什麼也不幹。特別是對於新手來說,過多的checked異常讓他感到無所適從。

?
1 2 3 4 5 6 7 try { …… Statement s = con.createStatement(); …… Catch(SQLException sqlEx){ sqlEx.PrintStackTrace(); }

或者

?
1 2 3 4 5 6 7 try { …… Statement s = con.createStatement(); …… Catch(SQLException sqlEx){ //什麼也不幹 }

checked異常導致了許多難以理解的程式碼產生

當開發人員必須去捕獲一個自己無法正確處理的checked異常,通常的是重新封裝成一個新的異常後再丟擲。這樣做並沒有為程式帶來任何好處。反而使程式碼晚難以理解。

就像我們使用JDBC程式碼那樣,需要處理非常多的try…catch.,真正有用的程式碼被包含在try…catch之內。使得理解這個方法變理困難起來

checked異常導致異常被不斷的封裝成另一個類異常後再丟擲

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public void methodA() throws ExceptionA{ ….. throw new ExceptionA(); } public void methodB() throws ExceptionB{ try { methodA(); …… } catch (ExceptionA ex){ throw new ExceptionB(ex); } } Public void methodC() throws ExceptinC{ try { methodB(); } catch (ExceptionB ex){ throw new ExceptionC(ex); } }

我們看到異常就這樣一層層無休止的被封裝和重新丟擲。

checked異常導致破壞介面方法

一個介面上的一個方法已被多個類使用,當為這個方法額外新增一個checked異常時,那麼所有呼叫此方法的程式碼都需要修改。
可見上面這些問題都是因為呼叫者無法正確的處理checked異常時而被迫去捕獲和處理,被迫封裝後再重新丟擲。這樣十分不方便,並不能帶來任何好處。在這種情況下通常使用unChecked異常。

chekced異常並不是無一是處,checked異常比傳統程式設計的錯誤返回值要好用得多。通過編譯器來確保正確的處理異常比通過返回值判斷要好得多。

如果一個異常是致命的,不可恢復的。或者呼叫者去捕獲它沒有任何益處,使用unChecked異常。

如果一個異常是可以恢復的,可以被呼叫者正確處理的,使用checked異常。

在使用unChecked異常時,必須在在方法宣告中詳細的說明該方法可能會丟擲的unChekced異常。由呼叫者自己去決定是否捕獲unChecked異常

倒底什麼時候使用checked異常,什麼時候使用unChecked異常?並沒有一個絕對的標準。但是筆者可以給出一些建議

當所有呼叫者必須處理這個異常,可以讓呼叫者進行重試操作;或者該異常相當於該方法的第二個返回值。使用checked異常。
這個異常僅是少數比較高階的呼叫者才能處理,一般的呼叫者不能正確的處理。使用unchecked異常。有能力處理的呼叫者可以進行高階處理,一般呼叫者乾脆就不處理。

這個異常是一個非常嚴重的錯誤,如資料庫連線錯誤,檔案無法開啟等。或者這些異常是與外部環境相關的。不是重試可以解決的。使用unchecked異常。因為這種異常一旦出現,呼叫者根本無法處理。

如果不能確定時,使用unchecked異常。並詳細描述可能會丟擲的異常,以讓呼叫者決定是否進行處理。

3. 設計一個新的異常類

在設計一個新的異常類時,首先看看是否真正的需要這個異常類。一般情況下儘量不要去設計新的異常類,而是儘量使用java中已經存在的異常類。

?
1 IllegalArgumentException, UnsupportedOperationException

不管是新的異常是chekced異常還是unChecked異常。我們都必須考慮異常的巢狀問題。

?
1 2 3 4 public void methodA() throws ExceptionA{ ….. throw new ExceptionA(); }

方法methodA宣告會丟擲ExceptionA.

public void methodB()throws ExceptionB

methodB宣告會丟擲ExceptionB,當在methodB方法中呼叫methodA時,ExceptionA是無法處理的,所以ExceptionA應該繼續往上丟擲。一個辦法是把methodB宣告會丟擲ExceptionA.但這樣已經改變了MethodB的方法簽名。一旦改變,則所有呼叫methodB的方法都要進行改變。

另一個辦法是把ExceptionA封裝成ExceptionB,然後再丟擲。如果我們不把ExceptionA封裝在ExceptionB中,就丟失了根異常資訊,使得無法跟蹤異常的原始出處。

?
1 2 3 4 5 6 7 8 public void methodB() throws ExceptionB{ try { methodA(); …… } catch (ExceptionA ex){ throw new ExceptionB(ex); } }

如上面的程式碼中,ExceptionB巢狀一個ExceptionA.我們暫且把ExceptionA稱為“起因異常”,因為ExceptionA導致了ExceptionB的產生。這樣才不使異常資訊丟失。

所以我們在定義一個新的異常類時,必須提供這樣一個可以包含巢狀異常的建構函式。並有一個私有成員來儲存這個“起因異常”。

?
1 2 3 4 5 6 7 8 9 10 11 12 13 public Class ExceptionB extends Exception{ private Throwable cause; public ExceptionB(String msg, Throwable ex){ super (msg); this .cause = ex; } public ExceptionB(String msg){ super (msg); } public ExceptionB(Throwable ex){ this .cause = ex; } }

當然,我們在呼叫printStackTrace方法時,需要把所有的“起因異常”的資訊也同時打印出來。所以我們需要覆寫printStackTrace方法來顯示全部的異常棧跟蹤。包括巢狀異常的棧跟蹤。

?
1 2 3 4 5 6 7 8 public void printStackTrace(PrintStrean ps){ if (cause == null ){ super .printStackTrace(ps); } else { ps.println( this ); cause.printStackTrace(ps); } }

一個完整的支援巢狀的checked異常類原始碼如下。我們在這裡暫且把它叫做NestedException

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public NestedException extends Exception{ private Throwable cause; public NestedException (String msg){ super (msg); } public NestedException(String msg, Throwable ex){ super (msg); This.cause = ex; } public Throwable getCause(){ return ( this .cause == null ? this : this .cause); } public getMessage(){ String message = super .getMessage(); Throwable cause = getCause(); if (cause != null ){ message = message + “;nested Exception is ” + cause; } return message; } public void printStackTrace(PrintStream ps){ if (getCause == null ){ super .printStackTrace(ps); } else { ps.println( this ); getCause().printStackTrace(ps); } } public void printStackTrace(PrintWrite pw){ if (getCause() == null ){ super .printStackTrace(pw); } else { pw.println( this ); getCause().printStackTrace(pw); } } public void printStackTrace(){ printStackTrace(System.error); } }

同樣要設計一個unChecked異常類也與上面一樣。只是需要繼承RuntimeException。

4. 如何記錄異常

作為一個大型的應用系統都需要用日誌檔案來記錄系統的執行,以便於跟蹤和記錄系統的執行情況。系統發生的異常理所當然的需要記錄在日誌系統中。

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public String getPassword(String userId) throws NoSuchUserException{ UserInfo user = userDao.queryUserById(userId); If(user == null ){ Logger.info(“找不到該使用者資訊,userId=”+userId); throw new NoSuchUserException(“找不到該使用者資訊,userId=”+userId); } else { return user.getPassword(); } } public void sendUserPassword(String userId) throws Exception { UserInfo user = null ; try { user = getPassword(userId); //…….. sendMail(); // } catch (NoSuchUserException ex)( logger.error(“找不到該使用者資訊:”+userId+ex); throw new Exception(ex); }

我們注意到,一個錯誤被記錄了兩次.在錯誤的起源位置我們僅是以info級別進行記錄。而在sendUserPassword方法中,我們還把整個異常資訊都記錄了。

筆者曾看到很多專案是這樣記錄異常的,不管三七二一,只有遇到異常就把整個異常全部記錄下。如果一個異常被不斷的封裝丟擲多次,那麼就被記錄了多次。那麼異常倒底該在什麼地方被記錄?

異常應該在最初產生的位置記錄!

如果必須捕獲一個無法正確處理的異常,僅僅是把它封裝成另外一種異常往上丟擲。不必再次把已經被記錄過的異常再次記錄。

如果捕獲到一個異常,但是這個異常是可以處理的。則無需要記錄異常

?
1 2 3 4 5 6 7 8 9 10 11 public Date getDate(String str){ Date applyDate = null ; SimpleDateFormat format = new SimpleDateFormat(“MM/dd/yyyy”); try { applyDate = format.parse(applyDateStr); } catch (ParseException ex){ //乎略,當格式錯誤時,返回null } return applyDate; }

捕獲到一個未記錄過的異常或外部系統異常時,應該記