<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    Vincent

    Vicent's blog
    隨筆 - 74, 文章 - 0, 評論 - 5, 引用 - 0
    數(shù)據(jù)加載中……

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

    不變對象具有許多能更方便地使用它們的特性,包括不嚴(yán)格的同步需求和不必考慮數(shù)據(jù)訛誤就能自由地共享和高速緩存對象引用。盡管不變性可能未必對于所有類都有意義,但大多數(shù)程序中至少有一些類將受益于不可變。在本月的 Java 理論與實踐中,Brian Goetz 說明了不變性的一些長處和構(gòu)造不變類的一些準(zhǔn)則。請在附帶的 論壇中與作者和其他讀者分享您關(guān)于本文的心得。(也可以單擊文章頂部或底部的“討論”來訪問論壇。)

    不變對象是指在實例化后其外部可見狀態(tài)無法更改的對象。Java 類庫中的 StringIntegerBigDecimal 類就是不變對象的示例 ― 它們表示在對象的生命期內(nèi)無法更改的單個值。

    不變性的長處

    如果正確使用不變類,它們會極大地簡化編程。因為它們只能處于一種狀態(tài),所以只要正確構(gòu)造了它們,就決不會陷入不一致的狀態(tài)。您不必復(fù)制或克隆不變對象,就能自由地共享和高速緩存對它們的引用;您可以高速緩存它們的字段或其方法的結(jié)果,而不用擔(dān)心值會不會變成失效的或與對象的其它狀態(tài)不一致。不變類通常產(chǎn)生最好的映射鍵。而且,它們本來就是線程安全的,所以不必在線程間同步對它們的訪問。

    自由高速緩存

    因為不變對象的值沒有更改的危險,所以可以自由地高速緩存對它們的引用,而且可以肯定以后的引用仍將引用同一個值。同樣地,因為它們的特性無法更改,所以您可以高速緩存它們的字段和其方法的結(jié)果。

    如果對象是可變的,就必須在存儲對其的引用時引起注意。請考慮清單 1 中的代碼,其中排列了兩個由調(diào)度程序執(zhí)行的任務(wù)。目的是:現(xiàn)在啟動第一個任務(wù),而在某一天啟動第二個任務(wù)。


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

    因為 Date 是可變的,所以 scheduleTask 方法必須小心地用防范措施將日期參數(shù)復(fù)制(可能通過 clone() )到它的內(nèi)部數(shù)據(jù)結(jié)構(gòu)中。不然, task1task2 可能都在明天執(zhí)行,這可不是所期望的。更糟的是,任務(wù)調(diào)度程序所用的內(nèi)部數(shù)據(jù)結(jié)構(gòu)會變成訛誤。在編寫象 scheduleTask() 這樣的方法時,極其容易忘記用防范措施復(fù)制日期參數(shù)。如果忘記這樣做,您就制造了一個難以捕捉的錯誤,這個錯誤不會馬上顯現(xiàn)出來,而且當(dāng)它暴露時人們要花較長的時間才會捕捉到。不變的 Date 類不可能發(fā)生這類錯誤。

    固有的線程安全

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

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

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

    把對象當(dāng)作參數(shù)的方法不應(yīng)變更那些對象的狀態(tài),除非文檔明確說明可以這樣做,或者實際上這些方法具有該對象的所有權(quán)。當(dāng)我們將一個對象傳遞給普通方法時,通常不希望對象返回時已被更改。但是,使用可變對象時,完全會是這樣的。如果將 java.awt.Point 傳遞給諸如 Component.setLocation() 的方法,根本不會阻止 setLocation 修改我們傳入的 Point 的位置,也不會阻止 setLocation 存儲對該點的引用并稍后在另一個方法中更改它。(當(dāng)然, Component 不這樣做,因為它不魯莽,但是并不是所有類都那么客氣。)現(xiàn)在, Point 的狀態(tài)已在我們不知道的情況下更改了,其結(jié)果具有潛在危險 ― 當(dāng)點實際上在另一個位置時,我們?nèi)哉J(rèn)為它在原來的位置。然而,如果 Point 是不變的,那么這種惡意的代碼就不能以如此令人混亂而危險的方法修改我們的程序狀態(tài)了。

    良好的鍵

    不變對象產(chǎn)生最好的 HashMapHashSet 鍵。有些可變對象根據(jù)其狀態(tài)會更改它們的 hashCode() 值(如清單 2 中的 StringHolder 示例類)。如果使用這種可變對象作為 HashSet 鍵,然后對象更改了其狀態(tài),那么就會對 HashSet 實現(xiàn)引起混亂 ― 如果枚舉集合,該對象仍將出現(xiàn),但如果用 contains() 查詢集合,它就可能不出現(xiàn)。無需多說,這會引起某些混亂的行為。說明這一情況的清單 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());
        }
    
    												
    										





    回頁首


    何時使用不變類

    不變類最適合表示抽象數(shù)據(jù)類型(如數(shù)字、枚舉類型或顏色)的值。Java 類庫中的基本數(shù)字類(如 IntegerLongFloat )都是不變的,其它標(biāo)準(zhǔn)數(shù)字類型(如 BigIntegerBigDecimal )也是不變的。表示復(fù)數(shù)或精度任意的有理數(shù)的類將比較適合于不變性。甚至包含許多離散值的抽象類型(如向量或矩陣)也很適合實現(xiàn)為不變類,這取決于您的應(yīng)用程序。

    Flyweight 模式

    不變性啟用了 Flyweight 模式,該模式利用共享使得用對象有效地表示大量細(xì)顆粒度的對象變得容易。例如,您可能希望用一個對象來表示字處理文檔中的每個字符或圖像中的每個像素,但這一策略的幼稚實現(xiàn)將會對內(nèi)存使用和內(nèi)存管理開銷產(chǎn)生高得驚人的花費。Flyweight 模式采用工廠方法來分配對不變的細(xì)顆粒度對象的引用,并通過僅使一個對象實例與字母“a”對應(yīng)來利用共享縮減對象數(shù)。有關(guān) Flyweight 模式的更多信息,請參閱經(jīng)典書籍 Design Patterns(Gamma 等著;請參閱 參考資料)。

    Java 類庫中不變性的另一個不錯的示例是 java.awt.Color 。在某些顏色表示法(如 RGB、HSB 或 CMYK)中,顏色通常表示為一組有序的數(shù)字值,但把一種顏色當(dāng)作顏色空間中的一個特異值,而不是一組有序的獨立可尋址的值更有意義,因此將 Color 作為不變類實現(xiàn)是有道理的。

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

    事件是另一個適合用不變類實現(xiàn)的好示例。事件的生命期較短,而且常常會在創(chuàng)建它們的線程以外的線程中消耗,所以使它們成為不變的是利大于弊。大多數(shù) AWT 事件類都沒有作為嚴(yán)格的不變類來實現(xiàn),而是可以有小小的修改。同樣地,在使用一定形式的消息傳遞以在組件間通信的系統(tǒng)中,使消息對象成為不變的或許是明智的。





    回頁首


    編寫不變類的準(zhǔn)則

    編寫不變類很容易。如果以下幾點都為真,那么類就是不變的:

    • 它的所有字段都是 final
    • 該類聲明為 final
    • 不允許 this 引用在構(gòu)造期間轉(zhuǎn)義
    • 任何包含對可變對象(如數(shù)組、集合或類似 Date 的可變類)引用的字段:
      • 是私有的
      • 從不被返回,也不以其它方式公開給調(diào)用程序
      • 是對它們所引用對象的唯一引用
      • 構(gòu)造后不會更改被引用對象的狀態(tài)

    最后一組要求似乎挺復(fù)雜的,但其基本上意味著如果要存儲對數(shù)組或其它可變對象的引用,就必須確保您的類對該可變對象擁有獨占訪問權(quán)(因為不然的話,其它類能夠更改其狀態(tài)),而且在構(gòu)造后您不修改其狀態(tài)。為允許不變對象存儲對數(shù)組的引用,這種復(fù)雜性是必要的,因為 Java 語言沒有辦法強制不對 final 數(shù)組的元素進行修改。注:如果從傳遞給構(gòu)造函數(shù)的參數(shù)中初始化數(shù)組引用或其它可變字段,您必須用防范措施將調(diào)用程序提供的參數(shù)或您無法確保具有獨占訪問權(quán)的其它信息復(fù)制到數(shù)組。否則,調(diào)用程序會在調(diào)用構(gòu)造函數(shù)之后,修改數(shù)組的狀態(tài)。清單 3 顯示了編寫一個存儲調(diào)用程序提供的數(shù)組的不變對象的構(gòu)造函數(shù)的正確方法(和錯誤方法)。


    清單 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 的標(biāo)準(zhǔn)實現(xiàn)使用 hashCode 值的惰性計算),這樣可能比嚴(yán)格的 final 類執(zhí)行得更好。如果類表示抽象類型(如數(shù)字類型或顏色)的值,那么您還會想實現(xiàn) hashCode()equals() 方法,這樣對象將作為 HashMapHashSet 中的一個鍵工作良好。要保持線程安全,不允許 this 引用從構(gòu)造函數(shù)中轉(zhuǎn)義是很重要的。





    回頁首


    偶爾更改的數(shù)據(jù)

    有些數(shù)據(jù)項在程序生命期中一直保持常量,而有些會頻繁更改。常量數(shù)據(jù)顯然符合不變性,而狀態(tài)復(fù)雜且頻繁更改的對象通常不適合用不變類來實現(xiàn)。那么有時會更改,但更改又不太頻繁的數(shù)據(jù)呢?有什么方法能讓 有時更改的數(shù)據(jù)獲得不變性的便利和線程安全的長處呢?

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

    除了在修改列表時, CopyOnWriteArrayList 并不變更基本數(shù)組,而是創(chuàng)建新數(shù)組且廢棄舊數(shù)組之外,它的行為與 ArrayList 類非常相似。這意味著當(dāng)調(diào)用程序獲得迭代器(迭代器在內(nèi)部保存對基本數(shù)組的引用)時,迭代器引用的數(shù)組實際上是不變的,從而可以無需同步或冒并發(fā)修改的風(fēng)險進行遍歷。這消除了在遍歷前克隆列表或在遍歷期間對列表進行同步的需要,這兩個操作都很麻煩、易于出錯,而且完全使性能惡化。如果遍歷比插入或除去更加頻繁(這在某些情況下是常有的事), CopyOnWriteArrayList 會提供更佳的性能和更方便的訪問。





    回頁首


    結(jié)束語

    使用不變對象比使用可變對象要容易得多。它們只能處于一種狀態(tài),所以始終是一致的,它們本來就是線程安全的,可以被自由地共享。使用不變對象可以徹底消除許多容易發(fā)生但難以檢測的編程錯誤,如無法在線程間同步訪問或在存儲對數(shù)組或?qū)ο蟮囊们盁o法克隆該數(shù)組或?qū)ο蟆T诰帉戭悤r,問問自己這個類是否可以作為不變類有效地實現(xiàn),總是值得的。您可能會對回答常常是肯定的而感到吃驚。

    posted on 2006-08-24 17:50 Binary 閱讀(211) 評論(0)  編輯  收藏 所屬分類: j2se

    主站蜘蛛池模板: 亚洲国产成人精品无码区在线观看 | 国产精品无码亚洲精品2021 | 久久国产色AV免费看| 国产成人麻豆亚洲综合无码精品| 久久久亚洲欧洲日产国码aⅴ| 亚洲AV无码无限在线观看不卡| 又粗又长又爽又长黄免费视频| 久久久久久久99精品免费| 国产精品亚洲а∨无码播放| 亚洲成A人片在线播放器| 免费下载成人电影| 亚洲欧洲日产国码无码网站 | 青青久久精品国产免费看| 五月天婷亚洲天综合网精品偷| 特级毛片A级毛片免费播放| 亚洲av无码乱码在线观看野外| 欧洲乱码伦视频免费国产| 亚洲国产精品无码久久一区二区| 久久免费视频精品| 亚洲1区1区3区4区产品乱码芒果| 一区二区三区免费电影| 亚洲日韩中文字幕在线播放| 全免费a级毛片免费看| 亚洲国产成+人+综合| 无码日韩精品一区二区免费暖暖| 亚洲精品综合一二三区在线| av无码国产在线看免费网站| 亚洲国产婷婷六月丁香| 8x成人永久免费视频| 亚洲高清毛片一区二区| 岛国片在线免费观看| 免费一级毛片在线播放放视频| 久久亚洲精品中文字幕三区| 五月婷婷在线免费观看| 日日摸夜夜添夜夜免费视频| 亚洲欧洲在线观看| 日本无吗免费一二区| 亚洲Av永久无码精品黑人| 亚洲日韩精品A∨片无码| 美女被免费喷白浆视频| 一级黄色免费大片|