Java 理論與實(shí)踐: 變還是不變?
不變對(duì)象具有許多能更方便地使用它們的特性,包括不嚴(yán)格的同步需求和不必考慮數(shù)據(jù)訛誤就能自由地共享和高速緩存對(duì)象引用。盡管不變性可能未必對(duì)于所有類都有意義,但大多數(shù)程序中至少有一些類將受益于不可變。在本月的 Java 理論與實(shí)踐中,Brian Goetz 說明了不變性的一些長(zhǎng)處和構(gòu)造不變類的一些準(zhǔn)則。請(qǐng)?jiān)诟綆У?論壇中與作者和其他讀者分享您關(guān)于本文的心得。(也可以單擊文章頂部或底部的“討論”來訪問論壇。)
不變對(duì)象是指在實(shí)例化后其外部可見狀態(tài)無法更改的對(duì)象。Java 類庫(kù)中的 String
、 Integer
和 BigDecimal
類就是不變對(duì)象的示例 ― 它們表示在對(duì)象的生命期內(nèi)無法更改的單個(gè)值。
如果正確使用不變類,它們會(huì)極大地簡(jiǎn)化編程。因?yàn)樗鼈冎荒芴幱谝环N狀態(tài),所以只要正確構(gòu)造了它們,就決不會(huì)陷入不一致的狀態(tài)。您不必復(fù)制或克隆不變對(duì)象,就能自由地共享和高速緩存對(duì)它們的引用;您可以高速緩存它們的字段或其方法的結(jié)果,而不用擔(dān)心值會(huì)不會(huì)變成失效的或與對(duì)象的其它狀態(tài)不一致。不變類通常產(chǎn)生最好的映射鍵。而且,它們本來就是線程安全的,所以不必在線程間同步對(duì)它們的訪問。
因?yàn)椴蛔儗?duì)象的值沒有更改的危險(xiǎn),所以可以自由地高速緩存對(duì)它們的引用,而且可以肯定以后的引用仍將引用同一個(gè)值。同樣地,因?yàn)樗鼈兊奶匦詿o法更改,所以您可以高速緩存它們的字段和其方法的結(jié)果。
如果對(duì)象是可變的,就必須在存儲(chǔ)對(duì)其的引用時(shí)引起注意。請(qǐng)考慮清單 1 中的代碼,其中排列了兩個(gè)由調(diào)度程序執(zhí)行的任務(wù)。目的是:現(xiàn)在啟動(dòng)第一個(gè)任務(wù),而在某一天啟動(dòng)第二個(gè)任務(wù)。
清單 1. 可變的 Date 對(duì)象的潛在問題
|
因?yàn)?Date
是可變的,所以 scheduleTask
方法必須小心地用防范措施將日期參數(shù)復(fù)制(可能通過 clone()
)到它的內(nèi)部數(shù)據(jù)結(jié)構(gòu)中。不然, task1
和 task2
可能都在明天執(zhí)行,這可不是所期望的。更糟的是,任務(wù)調(diào)度程序所用的內(nèi)部數(shù)據(jù)結(jié)構(gòu)會(huì)變成訛誤。在編寫象 scheduleTask()
這樣的方法時(shí),極其容易忘記用防范措施復(fù)制日期參數(shù)。如果忘記這樣做,您就制造了一個(gè)難以捕捉的錯(cuò)誤,這個(gè)錯(cuò)誤不會(huì)馬上顯現(xiàn)出來,而且當(dāng)它暴露時(shí)人們要花較長(zhǎng)的時(shí)間才會(huì)捕捉到。不變的 Date
類不可能發(fā)生這類錯(cuò)誤。
大多數(shù)的線程安全問題發(fā)生在當(dāng)多個(gè)線程正在試圖并發(fā)地修改一個(gè)對(duì)象的狀態(tài)(寫-寫沖突)時(shí),或當(dāng)一個(gè)線程正試圖訪問一個(gè)對(duì)象的狀態(tài),而另一個(gè)線程正在修改它(讀-寫沖突)時(shí)。要防止這樣的沖突,必須同步對(duì)共享對(duì)象的訪問,以便在對(duì)象處于不一致狀態(tài)時(shí)其它線程不能訪問它們。正確地做到這一點(diǎn)會(huì)很難,需要大量文檔來確保正確地?cái)U(kuò)展程序,還可能對(duì)性能產(chǎn)生不利后果。只要正確構(gòu)造了不變對(duì)象(這意味著不讓對(duì)象引用從構(gòu)造函數(shù)中轉(zhuǎn)義),就使它們免除了同步訪問的要求,因?yàn)闊o法更改它們的狀態(tài),從而就不可能存在寫-寫沖突或讀-寫沖突。
不用同步就能自由地在線程間共享對(duì)不變對(duì)象的引用,可以極大地簡(jiǎn)化編寫并發(fā)程序的過程,并減少程序可能存在的潛在并發(fā)錯(cuò)誤的數(shù)量。
把對(duì)象當(dāng)作參數(shù)的方法不應(yīng)變更那些對(duì)象的狀態(tài),除非文檔明確說明可以這樣做,或者實(shí)際上這些方法具有該對(duì)象的所有權(quán)。當(dāng)我們將一個(gè)對(duì)象傳遞給普通方法時(shí),通常不希望對(duì)象返回時(shí)已被更改。但是,使用可變對(duì)象時(shí),完全會(huì)是這樣的。如果將 java.awt.Point
傳遞給諸如 Component.setLocation()
的方法,根本不會(huì)阻止 setLocation
修改我們傳入的 Point
的位置,也不會(huì)阻止 setLocation 存儲(chǔ)對(duì)該點(diǎn)的引用并稍后在另一個(gè)方法中更改它。(當(dāng)然, Component
不這樣做,因?yàn)樗霍斆В遣⒉皇撬蓄惗寄敲纯蜌狻#┈F(xiàn)在, Point
的狀態(tài)已在我們不知道的情況下更改了,其結(jié)果具有潛在危險(xiǎn) ― 當(dāng)點(diǎn)實(shí)際上在另一個(gè)位置時(shí),我們?nèi)哉J(rèn)為它在原來的位置。然而,如果 Point
是不變的,那么這種惡意的代碼就不能以如此令人混亂而危險(xiǎn)的方法修改我們的程序狀態(tài)了。
不變對(duì)象產(chǎn)生最好的 HashMap
或 HashSet
鍵。有些可變對(duì)象根據(jù)其狀態(tài)會(huì)更改它們的 hashCode()
值(如清單 2 中的 StringHolder
示例類)。如果使用這種可變對(duì)象作為 HashSet
鍵,然后對(duì)象更改了其狀態(tài),那么就會(huì)對(duì) HashSet
實(shí)現(xiàn)引起混亂 ― 如果枚舉集合,該對(duì)象仍將出現(xiàn),但如果用 contains()
查詢集合,它就可能不出現(xiàn)。無需多說,這會(huì)引起某些混亂的行為。說明這一情況的清單 2 中的代碼將打印“false”、“1”和“moo”。
清單 2. 可變 StringHolder 類,不適合用作鍵
|
![]() ![]() |
![]()
|
不變類最適合表示抽象數(shù)據(jù)類型(如數(shù)字、枚舉類型或顏色)的值。Java 類庫(kù)中的基本數(shù)字類(如 Integer
、 Long
和 Float
)都是不變的,其它標(biāo)準(zhǔn)數(shù)字類型(如 BigInteger
和 BigDecimal
)也是不變的。表示復(fù)數(shù)或精度任意的有理數(shù)的類將比較適合于不變性。甚至包含許多離散值的抽象類型(如向量或矩陣)也很適合實(shí)現(xiàn)為不變類,這取決于您的應(yīng)用程序。
![]() |
|
Java 類庫(kù)中不變性的另一個(gè)不錯(cuò)的示例是 java.awt.Color
。在某些顏色表示法(如 RGB、HSB 或 CMYK)中,顏色通常表示為一組有序的數(shù)字值,但把一種顏色當(dāng)作顏色空間中的一個(gè)特異值,而不是一組有序的獨(dú)立可尋址的值更有意義,因此將 Color
作為不變類實(shí)現(xiàn)是有道理的。
如果要表示的對(duì)象是多個(gè)基本值的容器(如:點(diǎn)、向量、矩陣或 RGB 顏色),是用可變對(duì)象還是用不變對(duì)象表示?答案是……要看情況而定。要如何使用它們?它們主要用來表示多維值(如像素的顏色),還是僅僅用作其它對(duì)象的一組相關(guān)特性集合(如窗口的高度和寬度)的容器?這些特性多久更改一次?如果更改它們,那么各個(gè)組件值在應(yīng)用程序中是否有其自己的含義呢?
事件是另一個(gè)適合用不變類實(shí)現(xiàn)的好示例。事件的生命期較短,而且常常會(huì)在創(chuàng)建它們的線程以外的線程中消耗,所以使它們成為不變的是利大于弊。大多數(shù) AWT 事件類都沒有作為嚴(yán)格的不變類來實(shí)現(xiàn),而是可以有小小的修改。同樣地,在使用一定形式的消息傳遞以在組件間通信的系統(tǒng)中,使消息對(duì)象成為不變的或許是明智的。
![]() ![]() |
![]()
|
編寫不變類很容易。如果以下幾點(diǎn)都為真,那么類就是不變的:
- 它的所有字段都是 final
- 該類聲明為 final
- 不允許
this
引用在構(gòu)造期間轉(zhuǎn)義 - 任何包含對(duì)可變對(duì)象(如數(shù)組、集合或類似
Date
的可變類)引用的字段:- 是私有的
- 從不被返回,也不以其它方式公開給調(diào)用程序
- 是對(duì)它們所引用對(duì)象的唯一引用
- 構(gòu)造后不會(huì)更改被引用對(duì)象的狀態(tài)
最后一組要求似乎挺復(fù)雜的,但其基本上意味著如果要存儲(chǔ)對(duì)數(shù)組或其它可變對(duì)象的引用,就必須確保您的類對(duì)該可變對(duì)象擁有獨(dú)占訪問權(quán)(因?yàn)椴蝗坏脑挘渌惸軌蚋钠錉顟B(tài)),而且在構(gòu)造后您不修改其狀態(tài)。為允許不變對(duì)象存儲(chǔ)對(duì)數(shù)組的引用,這種復(fù)雜性是必要的,因?yàn)?Java 語(yǔ)言沒有辦法強(qiáng)制不對(duì) final 數(shù)組的元素進(jìn)行修改。注:如果從傳遞給構(gòu)造函數(shù)的參數(shù)中初始化數(shù)組引用或其它可變字段,您必須用防范措施將調(diào)用程序提供的參數(shù)或您無法確保具有獨(dú)占訪問權(quán)的其它信息復(fù)制到數(shù)組。否則,調(diào)用程序會(huì)在調(diào)用構(gòu)造函數(shù)之后,修改數(shù)組的狀態(tài)。清單 3 顯示了編寫一個(gè)存儲(chǔ)調(diào)用程序提供的數(shù)組的不變對(duì)象的構(gòu)造函數(shù)的正確方法(和錯(cuò)誤方法)。
清單 3. 對(duì)不變對(duì)象編碼的正確和錯(cuò)誤方法
|
通過一些其它工作,可以編寫使用一些非 final 字段的不變類(例如, String
的標(biāo)準(zhǔn)實(shí)現(xiàn)使用 hashCode
值的惰性計(jì)算),這樣可能比嚴(yán)格的 final 類執(zhí)行得更好。如果類表示抽象類型(如數(shù)字類型或顏色)的值,那么您還會(huì)想實(shí)現(xiàn) hashCode()
和 equals()
方法,這樣對(duì)象將作為 HashMap
或 HashSet
中的一個(gè)鍵工作良好。要保持線程安全,不允許 this
引用從構(gòu)造函數(shù)中轉(zhuǎn)義是很重要的。
![]() ![]() |
![]()
|
有些數(shù)據(jù)項(xiàng)在程序生命期中一直保持常量,而有些會(huì)頻繁更改。常量數(shù)據(jù)顯然符合不變性,而狀態(tài)復(fù)雜且頻繁更改的對(duì)象通常不適合用不變類來實(shí)現(xiàn)。那么有時(shí)會(huì)更改,但更改又不太頻繁的數(shù)據(jù)呢?有什么方法能讓 有時(shí)更改的數(shù)據(jù)獲得不變性的便利和線程安全的長(zhǎng)處呢?
util.concurrent
包中的 CopyOnWriteArrayList
類是如何既利用不變性的能力,又仍允許偶爾修改的一個(gè)良好示例。它最適合于支持事件監(jiān)聽程序的類(如用戶界面組件)使用。雖然事件監(jiān)聽程序的列表可以更改,但通常它更改的頻繁性要比事件的生成少得多。
除了在修改列表時(shí), CopyOnWriteArrayList
并不變更基本數(shù)組,而是創(chuàng)建新數(shù)組且廢棄舊數(shù)組之外,它的行為與 ArrayList
類非常相似。這意味著當(dāng)調(diào)用程序獲得迭代器(迭代器在內(nèi)部保存對(duì)基本數(shù)組的引用)時(shí),迭代器引用的數(shù)組實(shí)際上是不變的,從而可以無需同步或冒并發(fā)修改的風(fēng)險(xiǎn)進(jìn)行遍歷。這消除了在遍歷前克隆列表或在遍歷期間對(duì)列表進(jìn)行同步的需要,這兩個(gè)操作都很麻煩、易于出錯(cuò),而且完全使性能惡化。如果遍歷比插入或除去更加頻繁(這在某些情況下是常有的事), CopyOnWriteArrayList
會(huì)提供更佳的性能和更方便的訪問。
![]() ![]() |
![]()
|
使用不變對(duì)象比使用可變對(duì)象要容易得多。它們只能處于一種狀態(tài),所以始終是一致的,它們本來就是線程安全的,可以被自由地共享。使用不變對(duì)象可以徹底消除許多容易發(fā)生但難以檢測(cè)的編程錯(cuò)誤,如無法在線程間同步訪問或在存儲(chǔ)對(duì)數(shù)組或?qū)ο蟮囊们盁o法克隆該數(shù)組或?qū)ο蟆T诰帉戭悤r(shí),問問自己這個(gè)類是否可以作為不變類有效地實(shí)現(xiàn),總是值得的。您可能會(huì)對(duì)回答常常是肯定的而感到吃驚。
posted on 2006-08-24 17:50 Binary 閱讀(212) 評(píng)論(0) 編輯 收藏 所屬分類: j2se