Java 理論與實踐: 變仍是不變?

http://www.ibm.com/developerworks/cn/java/j-jtp02183/java


不變對象是指在實例化後其外部可見狀態沒法更改的對象。Java 類庫中的 String 、 Integer 和BigDecimal 類就是不變對象的示例 ― 它們表示在對象的生命期內沒法更改的單個值。編程

不變性的長處

若是正確使用不變類,它們會極大地簡化編程。由於它們只能處於一種狀態,因此只要正確構造了它們,就決不會陷入不一致的狀態。您沒必要複製或克隆不變對象,就能自由地共享和高速緩存對它們的引用;您能夠高速緩存它們的字段或其方法的結果,而不用擔憂值會不會變成失效的或與對象的其它狀態不一致。不變類一般產生最好的映射鍵。並且,它們原本就是線程安全的,因此沒必要在線程間同步對它們的訪問。數組

自由高速緩存

由於不變對象的值沒有更改的危險,因此能夠自由地高速緩存對它們的引用,並且能夠確定之後的引用仍將引用同一個值。一樣地,由於它們的特性沒法更改,因此您能夠高速緩存它們的字段和其方法的結果。緩存

若是對象是可變的,就必須在存儲對其的引用時引發注意。請考慮清單 1 中的代碼,其中排列了兩個由調度程序執行的任務。目的是:如今啓動第一個任務,而在某一天啓動第二個任務。安全

清單 1. 可變的 Date 對象的潛在問題
 Date d = new Date();
  Scheduler.scheduleTask(task1, d);
  d.setTime(d.getTime() + ONE_DAY);
  scheduler.scheduleTask(task2, d);

由於 Date 是可變的,因此 scheduleTask 方法必須當心地用防範措施將日期參數複製(可能經過 clone() )到它的內部數據結構中。否則, task1 和 task2 可能都在明天執行,這可不是所指望的。更糟的是,任務調度程序所用的內部數據結構會變成訛誤。在編寫象scheduleTask() 這樣的方法時,極其容易忘記用防範措施複製日期參數。若是忘記這樣作,您就製造了一個難以捕捉的錯誤,這個錯誤不會立刻顯現出來,並且當它暴露時人們要花較長的時間纔會捕捉到。不變的 Date 類不可能發生這類錯誤。數據結構

固有的線程安全

大多數的線程安全問題發生在當多個線程正在試圖併發地修改一個對象的狀態(寫-寫衝突)時,或當一個線程正試圖訪問一個對象的狀態,而另外一個線程正在修改它(讀-寫衝突)時。要防止這樣的衝突,必須同步對共享對象的訪問,以便在對象處於不一致狀態時其它線程不能訪問它們。正確地作到這一點會很難,須要大量文檔來確保正確地擴展程序,還可能對性能產生不利後果。只要正確構造了不變對象(這意味着不讓對象引用從構造函數中轉義),就使它們免除了同步訪問的要求,由於沒法更改它們的狀態,從而就不可能存在寫-寫衝突或讀-寫衝突。併發

不用同步就能自由地在線程間共享對不變對象的引用,能夠極大地簡化編寫併發程序的過程,並減小程序可能存在的潛在併發錯誤的數量。ide

在惡意運行的代碼面前是安全的

把對象看成參數的方法不該變動那些對象的狀態,除非文檔明確說明能夠這樣作,或者實際上這些方法具備該對象的全部權。當咱們將一個對象傳遞給普通方法時,一般不但願對象返回時已被更改。可是,使用可變對象時,徹底會是這樣的。若是將 java.awt.Point 傳遞給諸如Component.setLocation() 的方法,根本不會阻止 setLocation 修改咱們傳入的 Point 的位置,也不會阻止 setLocation 存儲對該點的引用並稍後在另外一個方法中更改它。(固然, Component 不這樣作,由於它不魯莽,可是並非全部類都那麼客氣。)如今, Point 的狀態已在咱們不知道的狀況下更改了,其結果具備潛在危險 ― 當點實際上在另外一個位置時,咱們仍認爲它在原來的位置。然而,若是 Point 是不變的,那麼這種惡意的代碼就不能以如此使人混亂而危險的方法修改咱們的程序狀態了。函數

良好的鍵

不變對象產生最好的 HashMap 或 HashSet 鍵。有些可變對象根據其狀態會更改它們的 hashCode() 值(如清單 2 中的 StringHolder 示例類)。若是使用這種可變對象做爲 HashSet 鍵,而後對象更改了其狀態,那麼就會對 HashSet 實現引發混亂 ― 若是枚舉集合,該對象仍將出現,但若是用 contains() 查詢集合,它就可能不出現。無需多說,這會引發某些混亂的行爲。說明這一狀況的清單 2 中的代碼將打印「false」、「1」和「moo」。性能

清單 2. 可變 StringHolder 類,不適合用做鍵
   public class StringHolder {
        private String string;
        public StringHolder(String s) {
            this.string = s;
        }
        public String getString() {
            return string;
        }
        public void setString(String string) {
            this.string = string;
        }
        public boolean equals(Object o) {
            if (this == o)
                return true;
            else if (o == null || !(o instanceof StringHolder))
                return false;
            else {
                final StringHolder other = (StringHolder) o;
                if (string == null)
                    return (other.string == null);
                else
                    return string.equals(other.string);
            }
        }
        public int hashCode() {
            return (string != null ? string.hashCode() : 0);
        }
        public String toString() {
            return string;
        }
        ...
        StringHolder sh = new StringHolder("blert");
        HashSet h = new HashSet();
        h.add(sh);
        sh.setString("moo");
        System.out.println(h.contains(sh));
        System.out.println(h.size());
        System.out.println(h.iterator().next());
    }

什麼時候使用不變類

不變類最適合表示抽象數據類型(如數字、枚舉類型或顏色)的值。Java 類庫中的基本數字類(如 Integer 、 Long 和 Float )都是不變的,其它標準數字類型(如 BigInteger 和 BigDecimal )也是不變的。表示複數或精度任意的有理數的類將比較適合於不變性。甚至包含許多離散值的抽象類型(如向量或矩陣)也很適合實現爲不變類,這取決於您的應用程序。

Flyweight 模式

不變性啓用了 Flyweight 模式,該模式利用共享使得用對象有效地表示大量細顆粒度的對象變得容易。例如,您可能但願用一個對象來表示字處理文檔中的每一個字符或圖像中的每一個像素,但這一策略的幼稚實現將會對內存使用和內存管理開銷產生高得驚人的花費。Flyweight 模式採用工廠方法來分配對不變的細顆粒度對象的引用,並經過僅使一個對象實例與字母「a」對應來利用共享縮減對象數。有關 Flyweight 模式的更多信息,請參閱經典書籍Design Patterns(Gamma 等著;請參閱 參考資料)。

Java 類庫中不變性的另外一個不錯的示例是 java.awt.Color 。在某些顏色表示法(如 RGB、HSB 或 CMYK)中,顏色一般表示爲一組有序的數字值,但把一種顏色看成顏色空間中的一個特異值,而不是一組有序的獨立可尋址的值更有意義,所以將 Color 做爲不變類實現是有道理的。

若是要表示的對象是多個基本值的容器(如:點、向量、矩陣或 RGB 顏色),是用可變對象仍是用不變對象表示?答案是……要看狀況而定。要如何使用它們?它們主要用來表示多維值(如像素的顏色),仍是僅僅用做其它對象的一組相關特性集合(如窗口的高度和寬度)的容器?這些特性多久更改一次?若是更改它們,那麼各個組件值在應用程序中是否有其本身的含義呢?

事件是另外一個適合用不變類實現的好示例。事件的生命期較短,並且經常會在建立它們的線程之外的線程中消耗,因此使它們成爲不變的是利大於弊。大多數 AWT 事件類都沒有做爲嚴格的不變類來實現,而是能夠有小小的修改。一樣地,在使用必定形式的消息傳遞以在組件間通訊的系統中,使消息對象成爲不變的或許是明智的。

編寫不變類的準則

編寫不變類很容易。若是如下幾點都爲真,那麼類就是不變的:

  • 它的全部字段都是 final
  • 該類聲明爲 final
  • 不容許 this 引用在構造期間轉義
  • 任何包含對可變對象(如數組、集合或相似 Date 的可變類)引用的字段:
    • 是私有的
    • 從不被返回,也不以其它方式公開給調用程序
    • 是對它們所引用對象的惟一引用
    • 構造後不會更改被引用對象的狀態

最後一組要求彷佛挺複雜的,但其基本上意味着若是要存儲對數組或其它可變對象的引用,就必須確保您的類對該可變對象擁有獨佔訪問權(由於否則的話,其它類可以更改其狀態),並且在構造後您不修改其狀態。爲容許不變對象存儲對數組的引用,這種複雜性是必要的,由於 Java 語言沒有辦法強制不對 final 數組的元素進行修改。注:若是從傳遞給構造函數的參數中初始化數組引用或其它可變字段,您必須用防範措施將調用程序提供的參數或您沒法確保具備獨佔訪問權的其它信息複製到數組。不然,調用程序會在調用構造函數以後,修改數組的狀態。清單 3 顯示了編寫一個存儲調用程序提供的數組的不變對象的構造函數的正確方法(和錯誤方法)。

清單 3. 對不變對象編碼的正確和錯誤方法
class ImmutableArrayHolder {
  private final int[] theArray;
  // Right way to write a constructor -- copy the array
  public ImmutableArrayHolder(int[] anArray) {
    this.theArray = (int[]) anArray.clone();
  }
  // Wrong way to write a constructor -- copy the reference
  // The caller could change the array after the call to the constructor
  public ImmutableArrayHolder(int[] anArray) {
    this.theArray = anArray;
  }
  // Right way to write an accessor -- don't expose the array reference
  public int getArrayLength() { return theArray.length }
  public int getArray(int n)  { return theArray[n]; }
  // Right way to write an accessor -- use clone()
  public int[] getArray()       { return (int[]) theArray.clone(); }
  // Wrong way to write an accessor -- expose the array reference
  // A caller could get the array reference and then change the contents
  public int[] getArray()       { return theArray }
}

經過一些其它工做,能夠編寫使用一些非 final 字段的不變類(例如, String 的標準實現使用 hashCode 值的惰性計算),這樣可能比嚴格的 final 類執行得更好。若是類表示抽象類型(如數字類型或顏色)的值,那麼您還會想實現 hashCode() 和 equals() 方法,這樣對象將做爲HashMap 或 HashSet 中的一個鍵工做良好。要保持線程安全,不容許 this 引用從構造函數中轉義是很重要的。

偶爾更改的數據

有些數據項在程序生命期中一直保持常量,而有些會頻繁更改。常量數據顯然符合不變性,而狀態複雜且頻繁更改的對象一般不適合用不變類來實現。那麼有時會更改,但更改又不太頻繁的數據呢?有什麼方法能讓 有時更改的數據得到不變性的便利和線程安全的長處呢?

util.concurrent 包中的 CopyOnWriteArrayList 類是如何既利用不變性的能力,又仍容許偶爾修改的一個良好示例。它最適合於支持事件監聽程序的類(如用戶界面組件)使用。雖然事件監聽程序的列表能夠更改,但一般它更改的頻繁性要比事件的生成少得多。

除了在修改列表時, CopyOnWriteArrayList 並不變動基本數組,而是建立新數組且廢棄舊數組以外,它的行爲與 ArrayList 類很是類似。這意味着當調用程序得到迭代器(迭代器在內部保存對基本數組的引用)時,迭代器引用的數組其實是不變的,從而能夠無需同步或冒併發修改的風險進行遍歷。這消除了在遍歷前克隆列表或在遍歷期間對列表進行同步的須要,這兩個操做都很麻煩、易於出錯,並且徹底使性能惡化。若是遍歷比插入或除去更加頻繁(這在某些狀況下是常有的事), CopyOnWriteArrayList 會提供更佳的性能和更方便的訪問。

結束語

使用不變對象比使用可變對象要容易得多。它們只能處於一種狀態,因此始終是一致的,它們原本就是線程安全的,能夠被自由地共享。使用不變對象能夠完全消除許多容易發生但難以檢測的編程錯誤,如沒法在線程間同步訪問或在存儲對數組或對象的引用前沒法克隆該數組或對象。在編寫類時,問問本身這個類是否能夠做爲不變類有效地實現,老是值得的。您可能會對回答經常是確定的而感到吃驚。

參考資料

相關文章
相關標籤/搜索