異常(Exception):指程式運行過程中出現的非正常現象。
1、 Java異常的異常處理機制
早期的情況:
早期使用的程式設計語言是沒有提供專門進行異常處理功能的,程式設計人員只能苦逼的使用條件陳述式對各種可能設想到的錯誤情況進行判斷,來捕捉特定的異常,然後進行相應的處理。這樣的處理方式,往往要整出大段大段的if…else語句。本來需要完成相應功能的代碼塊很小,但是加上這樣針對異常處理的條件陳述式使得代碼顯得非常臃腫,這樣一來代碼的可讀性和可維護性就下降了,而且有時候還會遺漏意想不到的異常情況。
Java的出現:
針對上面的情況Java提供了強大的異常處理機制,可以說為程式猿帶來了福音,Java的異常處理機制可以很方便的在程式中監視可能發生異常的程式塊,並將所有的異常處理代碼集中放在程式的某處,使完成正常功能的程式碼與進行異常處理的程式碼分開。通過異常處理機制,減少了編程人員的工作量,增強了異常處理的靈活性,並使得程式的可讀性和可維護性大大的提高了。
在Java的處理機制中,引入了一些用來描述和處理異常的類,每個異常類反映一類啟動並執行錯誤,在類的定義中包含了該類異常的資訊和對異常進行處理的方法。對程式啟動並執行過程中發生某一個異常現象時,系統就產生了一個與之相應的異常類對象,並交由系統中的相應機制進行處理,以避免系統崩潰或其他對系統有害的結果發生,保證了程式啟動並執行安全性。
2、 Java異常類的定義
Java中,把異常分為:錯誤(Error)和異常情況(Exception)。
錯誤(Error):指程式本身存在非法的情形,這些情形常常是因為代碼存在著問題而引起的。而且編程人員可以通過對程式進行更新仔細的檢查,把這種錯誤的情形減小到最小,從理論上講,這些情形是可以避免的。
異常情況(Exception):表示另外一種“非同尋常”的錯誤。這種錯誤通常是不可預測的。常見的異常情況包括記憶體不足,找不到所需要的檔案等。
Throwable類派生了兩個子類:Exception和Error。
Error類描述內部的錯誤,它由系統保留,程式不能拋出這個類型的對象,Error類的對象不可捕獲,不可以恢複,出錯時系統通知使用者並終止程式;
Exception類則供應程式的使用,所有的Java異常類都是系統類別庫中的Exception類的子類。
繼承關係:
Throwable類是所有異常類的父類,實現了Serializable介面。Throwable有兩個很重要的子類:Exception和Error,分別表示異常和錯誤。異常一般表示可檢測、可恢複或者在編碼中可以避免的問題,一般是比較輕度的錯誤;錯誤通常表示嚴重的問題,大多數情況下與代碼的執行無關,比如OutOfMemoryError。
其中受檢的異常表示期望調用者在異常發生時能夠採取一些恢複的措施,或者將該異常傳播出去。比如我們的TaoShareException,就屬於受檢的異常。當一個方法拋出受檢異常時,意思就是告訴調用者,要調用我這個方法,你必須捕獲我拋出的異常。當然調用者捕獲這個異常之後可以忽略,但這通常不是個好辦法,最起碼應該列印一條日誌。
運行時異常和錯誤屬於未受檢的可拋出結構,在行為上是相同的:他們都不需要被捕獲。這是因為當我們的程式拋出這兩種結構時,通常代表的是該錯誤不可被恢複,繼續執行下去有害無益,比如IndexOutOfBoundsException、ArithmeticException和OutOfMemoryError。IndexOutOfBoundsException表示數組或者字串超出了索引範圍,ArithmeticException表示運算異常(比如分母為0),OutOfMemoryError表示JVM沒有足夠的記憶體來分配。前兩種異常都屬於RuntimeException,表明這種異常屬於編程錯誤,我們應該在我們的程式中就避免這種問題,而不是通過捕獲來解決。後一種屬於錯誤,是我們無法通過編碼來解決的,這種錯誤通常都是比較嚴重的問題,一般是JVM出了問題。
2 Java異常處理原則
2.1 不要忽略異常
這是處理異常最基本的原則,當一個異常被拋出的時候,說明程式中某個地方出了問題,忽略異常可能達不到我們期望的效果。我們應該在程式中對出現的異常進行處理,或者是繼續拋出。至少,我們應該包含一條語句,將該異常記錄進日誌,便於我們將來的分析。
2.2 盡量少用異常,優先使用標準異常
異常的處理會有一定的開銷,除非是在出現異常的情況下,否則不要將異常用於普通的控制流程。比如,不要將異常用來檢測數組是否越界。如下:
| //摘自《Effective Java》 try{ int i=0; while(true) range[i++].climb(); }catch(ArrayIndexOutOfBoundsException e){ } |
還有,不要總是依賴異常來解決null 指標問題,我們在調用方法之前就應該判斷對象是否為null。注意要判斷所有需要的對象是否為null,我以前碰到一個問題就是只判斷了部分為null,如下代碼所示:
| If(baskItem.getStatus() != null && baskItem.getStatus().getStatus() != Status.TOP.getStatus()) |
結果在啟動並執行過程中,卻出現了baskItem為null的情況。當然這種情況是很少遇到的,但是在某些極端的情況下還是會遇到。
在使用到異常的地方,優先使用標準異常而不是自訂異常,這樣會使你的代碼更具可讀性(因為標準異常大家都熟悉),同時也減少了異常類。
常見的標準異常有:
IllegalArgumentException,這個異常表示調用者傳遞了不合適的參數。一般在檢測到參數不正確的時候我們可以拋出這個異常,或者是返回null值或false,結束方法。
NullPointerException,這個是我們最熟悉的異常了。如果調用者在某個不允許null值的參數中傳遞了null值,習慣的做法就是拋出NullPointerException異常。
IndexOutOfBoundsException,如果調用者在某個序列下標的參數中傳遞了越界的值,應該拋出的就是IndexOutOfBoundsException異常。比如訪問超過數組下標的數組元素。
ConcurrentModificationException,這個異常被設計在java.util包中,用來表示一個單線程的對象正在被並發的修改。
UnsupportedOperationException,這個異常表示當前對象不支援所請求的操作。比如在實作類別中沒有實現介面定義的方法,就會拋出這個異常。
NumberFormatException,這個異常表示資料格式有誤,還有一個ArithmeticException異常,表示算術異常,比如在除法運算中傳遞了0作為除數。
還有我們使用的DAOException異常,表示訪問資料庫出了問題。
2.3 每個方法拋出的異常都要有文檔
不僅要為每個方法建立文檔,為每個方法拋出的異常建立完善的文檔也是很有必要的。
始終要單獨聲明每個受檢的異常,並且利用Javadoc的@throws標記,準確地記錄產生每個異常的條件。如下所示:
| /** * Create a POIFSFileSystem from an InputStream * * @param stream the InputStream from which to read the data * * @exception IOException on errors reading, or on invalid data */ public POIFSFileSystem(final InputStream stream) throws IOException; |
(註: @throws以前的寫法是@ecxeption)
對於未受檢異常,只要使用@throws標籤記錄下可能拋出的異常以及條件就可以了。但不要使用throws關鍵字將該異常包含在方法的聲明中。這是因為使用throws關鍵字聲明的異常必須在調用者的代碼中進行捕獲,而未受檢的異常一般是不需要調用者捕獲的。
2.4 記錄所捕獲到異常的有用資訊
異常發生時,我們通常都會查看產生異常的原因來排查問題,所以在異常中包含儘可能有用的細節資訊對我們解決問題有很大的協助。還好,一般的異常都會記錄異常產生的類名、方法名和發生異常的位置,以及大量的堆棧資訊。
實際上僅有這些資訊也不能完全滿足我們的需求。有時候,我們通過異常能判斷是操作資料庫的時候出錯了,但我們還不能確定是我們的SQL語句有錯,還是沒有操作許可權,或者是資料庫操作過程中發生了其它錯誤。這時候我們可能希望能看到執行的SQL語句或其它中間狀態,記錄這些資訊也是很有必要的。有兩種方式可以實現,一種做法是在捕獲異常後再記錄這些資訊;另一種做法是在自訂異常類的構造器中引入這些資訊。比如我們可以在IndexOutOfBoundsException異常中定義一個如下的構造器。
| public IndexOutOfBoundsException(int lowerBound,int upperBound, int index){ super("Lower bound:" + lowerBound + ", Upper bound:" + upperBound + ", Index:" + index); this.lowerBound = lowerBound; this.upperBound = upperBound; this.index = index; } |
以上代碼摘自《Effective Java》,雖然JDK並沒有這樣做,但是Joshua Bloch還是極力推薦這種做法。
其實在SQLException裡是有比較詳細的資訊記錄的,看一下SQLException的原始碼就會發現它有一個如下的構造器:
| public SQLException(String reason, String sqlState, int vendorCode, Throwable cause) |
這個構造器為我們詳細的記錄了異常發生的原因。Reason表示異常發生的原因;sqlState是XOPEN或者SQL:2003(一種規範)關於異常分類的代碼;vendorCode是資料庫特定的異常代碼。
有了以上的資訊之後,我們就可以通過SQLException中的getSQLState()和getErrorCode()方法來獲得異常更精確的異常資訊。
2.5 努力使失敗保持原子性
失敗的原子性,意思就是失敗的方法調用應該使對象保持在調用之前的狀態。摘自《Effective Java》
這一點聽起來比較抽象,但是實際上很容易理解。試想想一種情況,當我們捕獲了受檢的異常之後,我們並不會終止我們的代碼繼續執行,而試圖從這個異常中恢複。但是有一個對象的值卻在異常中被改變了,這樣我們的下一步操作可能取到的是一個不期望得到的值。舉個例子,假如我們要統計進行了某種操作的分享,用下面的代碼,或許在我們擷取分享的時候就失敗了,但是計數器卻進行了加1,很明顯不是我們期望的結果。
| int count=0; for(long baskItemId:arraylist){ count++; BaskItem baskItem=this.getBaskItemById(baskItemId,buyerId); if(baskItem!=null){ …; } } |
為了避免異常所產生的對象改變,我們可以有很多預防辦法。
① 在執行操作之前對參數進行校正,如果參數不對就拋出異常,避免後續的操作改變對象的值。(實際上在一個方法開始之前進行參數校正是很好的做法,不僅可以避免異常帶來的對象改變,還可以避免不必要的開銷。)
② 調整處理順序,使得任何可能引起對象改變的操作在可能產生異常的調用之後。
③ 在捕獲異常之後編寫一段恢複的代碼,進行復原。
④ 拷貝一份對象,在對象的拷貝上執行操作。當執行完畢後用拷貝的對象替換原來的對象。
當然,並不是針對任何異常都需要保持失敗的原子性,如果方法拋出了某種錯誤,使失敗儲存原子性就沒有多大意義了。比如出現OutOfMemoryError,就沒必要再從錯誤中恢複了。
2.6 不要一次捕獲所有異常
在我們的代碼中,經常會看到如下一勞永逸的代碼。
| try { …… ……//拋出AException …… ……//拋出BException …… ……//拋出CException } catch (Exception e) { log.error("XXX error", e.getCause()); } |
這段代碼的特點是用一個catch子句捕獲了所有的異常,看上去省事很多。實際上是存在缺陷的。一是可能針對不同的異常有不同的恢複措施,而這裡的代碼讓我們無法區分異常,也就無法針對性的恢複。二是這種方式掩蓋了代碼中的RuntimeException異常,通常出現RuntimeException屬於編程錯誤,也就是這種方式掩蓋了我們的編程錯誤。
針對一段代碼中可能有多個調用會產生異常,我們可以分別捕獲每種異常,進行不同的處理。
2.7 對可恢複的情況使用受檢異常,對編程錯誤使用運行時異常
這一點在明確了受檢異常和未受檢異常的區別就不難理解了。受檢異常是在編程中必須捕獲的異常。我們捕獲異常的目的就是在異常發生時希望採取一些措施來恢複。那如果我們希望調用者在調用我們的方法出錯之後能有補救措施,我們就應該拋出受檢異常。而對於一些編程錯誤,我們希望的是調用者能夠在代碼中就避免,這時候就應該拋出未受檢的異常RuntimeException,提示調用者改進代碼。比如前面提到的IndexOutOfBoundsException異常,出現這類異常,就應該考慮在代碼中避免數組越界了。
2.8 在finally塊中釋放資源
針對有些操作,比如IO、Socket或者資料庫操作,發生異常的時候,我們還需要正確釋放佔用的資源。一般我們在finally塊中完成資源的釋放,值得注意的是在finally塊中的子句也可能會出現異常,我們在編碼的時候要盡量避免出現這種異常。如下所示:
| try{ …… reader = new BufferedReader(new InputStreamReader( new FileInputStream(file))); for (String record = reader.readLine(); record != null { …… } } catch (FileNotFoundException e) { log.error("error while reading file " + file.getName()); } catch (IOException e) { log.error("error while reading file " + file.getName() + e); } catch (Exception e) { log.error("baskitem 任務失敗…"); } finally { try { reader.close(); } catch (IOException e) { log.error("error while closeing file " + file.getName()); } } |