本章是從《Effective Java》摘錄整理出來的關於異常處理的幾條建議。內容包括:
第1條: 只針對不正常的狀況才使用異常
第2條: 對於可恢復的條件使用被檢查的異常,對於程序錯誤使用運行時異常
第3條: 避免沒必要要的使用被檢查的異常
第4條: 儘可能使用標準的異常
第5條: 拋出的異常要適合於相應的抽象
第6條: 每一個方法拋出的異常都要有文檔
第7條: 在細節消息中包含失敗 -- 捕獲消息
第8條: 努力使失敗保持原子性
第9條: 不要忽略異常html
它們對應原書中"第8章 異常"部分的第39-47條。java
轉載請註明出處:http://www.cnblogs.com/skywang12345/p/3544287.html程序員
建議:異常只應該被用於不正常的條件,它們永遠不該該被用於正常的控制流。
經過比較下面的兩份代碼進行說明。
代碼1數組
try { int i=0; while (true) { arr[i]=0; i++; } } catch (IndexOutOfBoundsException e) { }
代碼2架構
for (int i=0; i<arr.length; i++) { arr[i]=0; }
兩份代碼的做用都是遍歷arr數組,並設置數組中每個元素的值爲0。代碼1的是經過異常來終止,看起來很是難懂,代碼2是經過數組邊界來終止。咱們應該避免使用代碼1這種方式,主要緣由有三點:
• 異常機制的設計初衷是用於不正常的狀況,因此不多會會JVM實現試圖對它們的性能進行優化。因此,建立、拋出和捕獲異常的開銷是很昂貴的。
• 把代碼放在try-catch中返回阻止了JVM實現原本可能要執行的某些特定的優化。
• 對數組進行遍歷的標準模式並不會致使冗餘的檢查,有些現代的JVM實現會將它們優化掉。併發
實際上,基於異常的模式比標準模式要慢得多。測試代碼以下:框架
public class Advice1 { private static int[] arr = new int[]{1,2,3,4,5}; private static int SIZE = 10000; public static void main(String[] args) { long s1 = System.currentTimeMillis(); for (int i=0; i<SIZE; i++) endByRange(arr); long e1 = System.currentTimeMillis(); System.out.println("endByRange time:"+(e1-s1)+"ms" ); long s2 = System.currentTimeMillis(); for (int i=0; i<SIZE; i++) endByException(arr); long e2 = System.currentTimeMillis(); System.out.println("endByException time:"+(e2-s2)+"ms" ); } // 遍歷arr數組: 經過異常的方式 private static void endByException(int[] arr) { try { int i=0; while (true) { arr[i]=0; i++; //System.out.println("endByRange: arr["+i+"]="+arr[i]); } } catch (IndexOutOfBoundsException e) { } } // 遍歷arr數組: 經過邊界的方式 private static void endByRange(int[] arr) { for (int i=0; i<arr.length; i++) { arr[i]=0; //System.out.println("endByException: arr["+i+"]="+arr[i]); } } }
運行結果:性能
endByRange time:8ms
endByException time:16ms
結果說明:經過異常遍歷的速度比普通方式遍歷數組慢不少!學習
• 運行時異常 -- RuntimeException類及其子類都被稱爲運行時異常。
• 被檢查的異常 -- Exception類自己,以及Exception的子類中除了"運行時異常"以外的其它子類都屬於被檢查異常。測試
它們的區別是:Java編譯器會對"被檢查的異常"進行檢查,而對"運行時異常"不會檢查。也就是說,對於被檢查的異常,要麼經過throws進行聲明拋出,要麼經過try-catch進行捕獲處理,不然不能經過編譯。而對於運行時異常,假若既"沒有經過throws聲明拋出它",也"沒有用try-catch語句捕獲它",仍是會編譯經過。固然,雖然說Java編譯器不會檢查運行時異常,可是,咱們一樣能夠經過throws對該異常進行說明,或經過try-catch進行捕獲。
ArithmeticException(例如,除數爲0),IndexOutOfBoundsException(例如,數組越界)等都屬於運行時異常。對於這種異常,咱們應該經過修改代碼進行避免它的產生。而對於被檢查的異常,則能夠經過處理讓程序恢復運行。例如,假設由於一個用戶沒有存儲足夠數量的前,因此他在企圖在一個收費電話上進行呼叫就會失敗;因而就將一個被檢查異常拋出。
"被檢查的異常"是Java語言的一個很好的特性。與返回代碼不一樣,"被檢查的異常"會強迫程序員處理例外的條件,大大提升了程序的可靠性。
可是,過度使用被檢查異常會使API用起來很是不方便。若是一個方法拋出一個或多個被檢查的異常,那麼調用該方法的代碼則必須在一個或多個catch語句塊中處理這些異常,或者必須經過throws聲明拋出這些異常。 不管是經過catch處理,仍是經過throws聲明拋出,都給程序員添加了不可忽略的負擔。
適用於"被檢查的異常"必須同時知足兩個條件:第一,即便正確使用API並不能阻止異常條件的發生。第二,一旦產生了異常,使用API的程序員能夠採起有用的動做對程序進行處理。
代碼重用是值得提倡的,這是一條通用規則,異常也不例外。重用現有的異常有幾個好處:
第一,它使得你的API更加易於學習和使用,由於它與程序員原來已經熟悉的習慣用法是一致的。
第二,對於用到這些API的程序而言,它們的可讀性更好,由於它們不會充斥着程序員不熟悉的異常。
第三,異常類越少,意味着內存佔用越小,而且轉載這些類的時間開銷也越小。
Java標準異常中有幾個是常常被使用的異常。以下表格:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ 異常 ┃ 使用場合 ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃ IllegalArgumentException │ 參數的值不合適 ┃
┠───────────────────────────┼─────────────────────────────┨
┃ IllegalStateException │ 參數的狀態不合適 ┃
┠───────────────────────────┼─────────────────────────────┨
┃ NullPointerException │ 在null被禁止的狀況下參數值爲null ┃
┠───────────────────────────┼─────────────────────────────┨
┃ IndexOutOfBoundsException │ 下標越界 ┃
┠───────────────────────────┼─────────────────────────────┨
┃ ConcurrentModificationException │ 在禁止併發修改的狀況下,對象檢測到併發修改 ┃
┠───────────────────────────┼──────────── ─────────────────┨
┃ UnsupportedOperationException │ 對象不支持客戶請求的方法 ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
雖然它們是Java平臺庫迄今爲止最常被重用的異常,可是,在許可的條件下,其它的異常也能夠被重用。例如,若是你要實現諸如複數或者矩陣之類的算術對象,那麼重用ArithmeticException和NumberFormatException將是很是合適的。若是一個異常知足你的須要,則不要猶豫,使用就能夠,不過你必定要確保拋出異常的條件與該異常的文檔中描述的條件一致。這種重用必須創建在語義的基礎上,而不是名字的基礎上!
最後,必定要清楚,選擇重用哪種異常並無必須遵循的規則。例如,考慮紙牌對象的情形,假設有一個用於發牌操做的方法,它的參數(handSize)是發一手牌的紙牌張數。假設調用者在這個參數中傳遞的值大於整副牌的剩餘張數。那麼這種情形既能夠被解釋爲IllegalArgumentException(handSize的值太大),也能夠被解釋爲IllegalStateException(相對客戶的請求而言,紙牌對象的紙牌太少)。
若是一個方法拋出的異常與它執行的任務沒有明顯的關聯關係,這種情形會讓人不知所措。當一個方法傳遞一個由低層抽象拋出的異常時,每每會發生這種狀況。這種狀況發生時,不只讓人困惑,並且也"污染"了高層API。
爲了不這個問題,高層實現應該捕獲低層的異常,同時拋出一個能夠按照高層抽象進行介紹的異常。這種作法被稱爲"異常轉譯(exception translation)"。
例如,在Java的集合框架AbstractSequentialList的get()方法以下(基於JDK1.7.0_40):
public E get(int index) { try { return listIterator(index).next(); } catch (NoSuchElementException exc) { throw new IndexOutOfBoundsException("Index: "+index); } }
listIterator(index)會返回ListIterator對象,調用該對象的next()方法可能會拋出NoSuchElementException異常。而在get()方法中,拋出NoSuchElementException異常會讓人感到困惑。因此,get()對NoSuchElementException進行了捕獲,並拋出了IndexOutOfBoundsException異常。即,至關於將NoSuchElementException轉譯成了IndexOutOfBoundsException異常。
要單獨的聲明被檢查的異常,而且利用Javadoc的@throws標記,準確地記錄下每一個異常被拋出的條件。
若是一個類中的許多方法處於一樣的緣由而拋出同一個異常,那麼在該類的文檔註釋中對這個異常作文檔,而不是爲每一個方法單獨作文檔,這是能夠接受的。
簡而言之,當咱們自定義異常或者拋出異常時,應該包含失敗相關的信息。
當一個程序因爲一個未被捕獲的異常而失敗的時候,系統會自動打印出該異常的棧軌跡。在棧軌跡中包含該異常的字符串表示。典型狀況下它包含該異常類的類名,以及緊隨其後的細節消息。
當一個對象拋出一個異常以後,咱們總指望這個對象仍然保持在一種定義良好的可用狀態之中。對於被檢查的異常而言,這尤其重要,由於調用者一般指望從被檢查的異常中恢復過來。
通常而言,一個失敗的方法調用應該保持使對象保持在"它在被調用以前的狀態"。具備這種屬性的方法被稱爲具備"失敗原子性(failure atomic)"。能夠理解爲,失敗了還保持着原子性。對象保持"失敗原子性"的方式有幾種:
(01) 設計一個非可變對象。
(02) 對於在可變對象上執行操做的方法,得到"失敗原子性"的最多見方法是,在執行操做以前檢查參數的有效性。以下(Stack.java中的pop方法):
public Object pop() { if (size==0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; return result; }
(03) 與上一種方法相似,能夠對計算處理過程調整順序,使得任何可能會失敗的計算部分都發生在對象狀態被修改以前。
(04) 編寫一段恢復代碼,由它來解釋操做過程當中發生的失敗,以及使對象回滾到操做開始以前的狀態上。
(05) 在對象的一份臨時拷貝上執行操做,當操做完成以後再把臨時拷貝中的結果複製給原來的對象。
雖然"保持對象的失敗原子性"是指望目標,但它並不老是能夠作獲得。例如,若是多個線程企圖在沒有適當的同步機制的狀況下,併發的訪問一個對象,那麼該對象就有可能被留在不一致的狀態中。
即便在能夠實現"失敗原子性"的場合,它也不是總被指望的。對於某些操做,它會顯著的增長開銷或者複雜性。
總的規則是:做爲方法規範的一部分,任何一個異常都不該該改變對象調用該方法以前的狀態,若是這條規則被違反,則API文檔中應該清楚的指明對象將會處於什麼樣的狀態。
當一個API的設計者聲明一個方法會拋出某個異常的時候,他們正在試圖說明某些事情。因此,請不要忽略它!忽略異常的代碼以下:
try { ... } catch (SomeException e) { }
空的catch塊會使異常達不到應有的目的,異常的目的是強迫你處理不正常的條件。忽略一個異常,就如同忽略一個火警信號同樣 -- 若把火警信號器關閉了,那麼當真正的火災發生時,就沒有人看到火警信號了。因此,至少catch塊應該包含一條說明,用來解釋爲何忽略這個異常是合適的。
更多內容