Java異常(二) 《Effective Java》中關於異常處理的幾條建議


 

概要

本章是從《Effective Java》摘錄整理出來的關於異常處理的幾條建議。內容包括:
第1條: 只針對不正常的情況才使用異常
第2條: 對於可恢復的條件使用被檢查的異常,對於程序錯誤使用運行時異常
第3條: 避免不必要的使用被檢查的異常
第4條: 盡量使用標准的異常
第5條: 拋出的異常要適合於相應的抽象
第6條: 每個方法拋出的異常都要有文檔
第7條: 在細節消息中包含失敗 -- 捕獲消息
第8條: 努力使失敗保持原子性
第9條: 不要忽略異常

它們對應原書中"第8章 異常"部分的第39-47條。

轉載請注明出處:http://www.cnblogs.com/skywang12345/p/3544287.html

 

第1條: 只針對不正常的情況才使用異常

建議:異常只應該被用於不正常的條件,它們永遠不應該被用於正常的控制流。
通過比較下面的兩份代碼進行說明。
代碼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

結果說明:通過異常遍歷的速度比普通方式遍歷數組慢很多!

 

第2條: 對於可恢復的條件使用被檢查的異常,對於程序錯誤使用運行時異常

運行時異常     -- RuntimeException類及其子類都被稱為運行時異常。
被檢查的異常 -- Exception類本身,以及Exception的子類中除了"運行時異常"之外的其它子類都屬於被檢查異常。

  它們的區別是:Java編譯器會對"被檢查的異常"進行檢查,而對"運行時異常"不會檢查。也就是說,對於被檢查的異常,要么通過throws進行聲明拋出,要么通過try-catch進行捕獲處理,否則不能通過編譯。而對於運行時異常,倘若既"沒有通過throws聲明拋出它",也"沒有用try-catch語句捕獲它",還是會編譯通過。當然,雖說Java編譯器不會檢查運行時異常,但是,我們同樣可以通過throws對該異常進行說明,或通過try-catch進行捕獲。
  ArithmeticException(例如,除數為0),IndexOutOfBoundsException(例如,數組越界)等都屬於運行時異常。對於這種異常,我們應該通過修改代碼進行避免它的產生。而對於被檢查的異常,則可以通過處理讓程序恢復運行。例如,假設因為一個用戶沒有存儲足夠數量的前,所以他在企圖在一個收費電話上進行呼叫就會失敗;於是就將一個被檢查異常拋出。

 

第3條: 避免不必要的使用被檢查的異常

  "被檢查的異常"是Java語言的一個很好的特性。與返回代碼不同,"被檢查的異常"會強迫程序員處理例外的條件,大大提高了程序的可靠性。
  但是,過分使用被檢查異常會使API用起來非常不方便。如果一個方法拋出一個或多個被檢查的異常,那么調用該方法的代碼則必須在一個或多個catch語句塊中處理這些異常,或者必須通過throws聲明拋出這些異常。 無論是通過catch處理,還是通過throws聲明拋出,都給程序員添加了不可忽略的負擔。

  適用於"被檢查的異常"必須同時滿足兩個條件:第一,即使正確使用API並不能阻止異常條件的發生。第二,一旦產生了異常,使用API的程序員可以采取有用的動作對程序進行處理。

 

第4條: 盡量使用標准的異常

代碼重用是值得提倡的,這是一條通用規則,異常也不例外。重用現有的異常有幾個好處:
第一,它使得你的API更加易於學習和使用,因為它與程序員原來已經熟悉的習慣用法是一致的。
第二,對於用到這些API的程序而言,它們的可讀性更好,因為它們不會充斥着程序員不熟悉的異常。
第三,異常類越少,意味着內存占用越小,並且轉載這些類的時間開銷也越小。

Java標准異常中有幾個是經常被使用的異常。如下表格:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ 異常                                                                  ┃ 使用場合                                                              ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃ IllegalArgumentException                             │ 參數的值不合適                                                    ┃
┠───────────────────────────┼─────────────────────────────┨
┃ IllegalStateException                                     │ 參數的狀態不合適                                                 ┃
┠───────────────────────────┼─────────────────────────────┨
┃ NullPointerException                                      │ 在null被禁止的情況下參數值為null                     ┃
┠───────────────────────────┼─────────────────────────────┨
┃ IndexOutOfBoundsException                       │ 下標越界                                                               ┃
┠───────────────────────────┼─────────────────────────────┨
┃ ConcurrentModificationException                │ 在禁止並發修改的情況下,對象檢測到並發修改 ┃
┠───────────────────────────┼──────────── ─────────────────┨
┃ UnsupportedOperationException                │ 對象不支持客戶請求的方法                                   ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
  雖然它們是Java平台庫迄今為止最常被重用的異常,但是,在許可的條件下,其它的異常也可以被重用。例如,如果你要實現諸如復數或者矩陣之類的算術對象,那么重用ArithmeticException和NumberFormatException將是非常合適的。如果一個異常滿足你的需要,則不要猶豫,使用就可以,不過你一定要確保拋出異常的條件與該異常的文檔中描述的條件一致。這種重用必須建立在語義的基礎上,而不是名字的基礎上!
  最后,一定要清楚,選擇重用哪一種異常並沒有必須遵循的規則。例如,考慮紙牌對象的情形,假設有一個用於發牌操作的方法,它的參數(handSize)是發一手牌的紙牌張數。假設調用者在這個參數中傳遞的值大於整副牌的剩余張數。那么這種情形既可以被解釋為IllegalArgumentException(handSize的值太大),也可以被解釋為IllegalStateException(相對客戶的請求而言,紙牌對象的紙牌太少)。

 

第5條: 拋出的異常要適合於相應的抽象

  如果一個方法拋出的異常與它執行的任務沒有明顯的關聯關系,這種情形會讓人不知所措。當一個方法傳遞一個由低層抽象拋出的異常時,往往會發生這種情況。這種情況發生時,不僅讓人困惑,而且也"污染"了高層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異常。

 

第6條: 每個方法拋出的異常都要有文檔

  要單獨的聲明被檢查的異常,並且利用Javadoc的@throws標記,准確地記錄下每個異常被拋出的條件。
  如果一個類中的許多方法處於同樣的原因而拋出同一個異常,那么在該類的文檔注釋中對這個異常做文檔,而不是為每個方法單獨做文檔,這是可以接受的。

 

第7條: 在細節消息中包含失敗 -- 捕獲消息

  簡而言之,當我們自定義異常或者拋出異常時,應該包含失敗相關的信息。
  當一個程序由於一個未被捕獲的異常而失敗的時候,系統會自動打印出該異常的棧軌跡。在棧軌跡中包含該異常的字符串表示。典型情況下它包含該異常類的類名,以及緊隨其后的細節消息。

 

第8條: 努力使失敗保持原子性

  當一個對象拋出一個異常之后,我們總期望這個對象仍然保持在一種定義良好的可用狀態之中。對於被檢查的異常而言,這尤為重要,因為調用者通常期望從被檢查的異常中恢復過來。
  一般而言,一個失敗的方法調用應該保持使對象保持在"它在被調用之前的狀態"。具有這種屬性的方法被稱為具有"失敗原子性(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文檔中應該清楚的指明對象將會處於什么樣的狀態。

 

第9條: 不要忽略異常

  當一個API的設計者聲明一個方法會拋出某個異常的時候,他們正在試圖說明某些事情。所以,請不要忽略它!忽略異常的代碼如下:

try {
    ...
} catch (SomeException e) {
}

  空的catch塊會使異常達不到應有的目的,異常的目的是強迫你處理不正常的條件。忽略一個異常,就如同忽略一個火警信號一樣 -- 若把火警信號器關閉了,那么當真正的火災發生時,就沒有人看到火警信號了。所以,至少catch塊應該包含一條說明,用來解釋為什么忽略這個異常是合適的。

  


更多內容

1. java異常(一) 基礎和架構

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM