程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java理論與實踐: 變還是不變?

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

編輯:關於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 類不可能發生這類錯誤。

固有的線程安全

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

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

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

把對象當作參數的方法不應變更那些對象的狀態,除非文檔明確說明可以這 樣做,或者實際上這些方法具有該對象的所有權。當我們將一個對象傳遞給普通 方法時,通常不希望對象返回時已被更改。但是,使用可變對象時,完全會是這 樣的。如果將 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 )也是不變的。表示復數或精度任意的 有理數的類將比較適合於不變性。甚至包含許多離散值的抽象類型(如向量或矩 陣)也很適合實現為不變類,這取決於您的應用程序。

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 會提供更佳的性能和更方便的訪問 。

結束語

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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved