immutable和mutable對象


看<Effective Java>時,有多個條目是關於或涉及到Immutable object的。作者非常推崇使用immutalbe object,而非與之對應的imuttable object。這里總結一下自己的理解。

先舉個例子,例如我們想實現一個字符串類string,在初始化的時候,我們用new string("hello")給它賦一個初值"hello"。后來在使用過程中,我們發現其值需要做改變為“world”。那該如何做呢?一種方式是直接在當前對象中修過內部成員,提供一個string.setValue函數;另外一種方式是創建一個新的字符串對象new string("world"),而保證之前的字符串對象不變。這里前者就是mutable object,因為在其初始化之后可變;后者是immutable object,因為一旦初始化后,其內部狀態就不會發生任何變化,如果需要變化,就必須從頭創建一個新的對象。而這相比較,各有什么優劣呢?

  • immutable object理解和實現上簡單。顧名思義,Immutable object表示不可變的對象。或者說,對象一旦創建起來,就只能被訪問,不能被修改。程序中經常使用的數據類都可以作為為immutable object。例如整數,字符串,顏色,時間,實體類等。Immuable類不能也不需要提供setter方法,只提供getter類方法或者其他不修改內部狀態的方法。而mutable object,必須提供setter方法以修改內部狀態。當內部變量比較多而且相互關聯時,設計好這些setter並非一件易事。
  • Immutable object可以確保線程安全。多線程訪問同一個對象,可能會因為一個正在讀另一個正在寫,或者兩個都在寫而導致並發問題。而對於immutable object,因為只有在一個線程創建時可以初始化其內部變量,所以后續多線程並發使用時,都只是不可變的操作,不會改變內部狀態。所以可以不加任何鎖的隨意訪問;而對於mutable object,因為有寫操作,所以如果訪問,必須小心的加鎖。
  • Immutable object不需要保護性拷貝(deffensive copy)。對象可以在函數之間傳來傳去。如果一個對象可以在多處被修改,那使用者很可能無法知曉對象目前的狀態。所以在參數傳遞的過程中,鼓勵采用拷貝一份再傳出去的辦法來保證對象只在一個地方被修改。對於immutable object,因為它在初始化之后不可修改,所以根本不用關心這個被不知道的類不小心或者惡意修改的問題;而對於mutable object,必須采用保護性拷貝以保證修改可控。例如mutable string用來表示中文名“張三”,它調用了一個打印拼音名字的函數,但該函數不小心直接調用了name.setValue("zhangsan"),那調用者現在看到的字符串的內容也是“zhangsan"了,這並不是我們希望的。
  • Immuable object不需要拷貝構造函數和clone。拷貝構造函數和clone的目的就是創建一個一樣的類。而之所以要創建一樣的類,是因為需要對兩個類分別作修改。所以對於immutable object,因為不能修改,所以也就不需要拷貝構造函數和clone。mutable object則很可能依賴於這類拷貝機制jinxing保護性拷貝等工作。
  • Immuable object更容易class invariant(類不變量)。只需要在構造的時候指定關心類不變量,后續不可修改,所以就不用關心了;單獨與mutable object,則在內部成員比較多的情況下,一個setter只設置了部分成員,而導致成員之間的正確關系被打破,從而使對象處於一個不穩定的狀態。例如假設mutable string有一個content表示內容,一個length表示長度。如果setValue只修改了content而忘記修改length,則會導致奇怪問題。
  • Immuable object只需要計算一次hashCode。因為immuable object的內部成員不發生變化,所以可以只計算一次,后續直接使用就可以了。而mutable object,必須每次計算。
  • Immutable object可能會有性能問題。例如一個對象有多個內部變量。我們只想修改一個。此時如果使用mutable object,就只修改一個就行了;但如果immutable,則必須從頭初始化所有的變量。還有一種情況是對於多階段的操作,例如多個字符串相加,如果使用immutable object,每兩個相加都需要創建新的對象;但如果使用mutable object,則只需要不斷的設置新的value就行了。

考慮到這些因素,在實現過程中,如果有的選擇,而且不會產生性能問題,我們可以盡量使用immutable object。如果真有性能問題,那經常采用的策略是提供immutable object,同時再提供一個輔助的mutable object。在需要高性能計算時,使用mutable object進行計算,然后把結果在轉換成immutable object。一個典型的例子就是String和StringBuilder。

 那如何實現一個immutable object?要做到下面幾點

  • 不提供setter
  • 設置類為final,使其不被子類化。
  • 設置所有成員變量(域)為final和private,防止直接修改。
  • 如果某些成員變量是immuable的,那這些變量一定不能被外界訪問到。如果要訪問,要使用保護性拷貝來復制一份傳給外界。

通過這些方式,可以保證對象只有在構造時被初始化一次,之后就變成只讀的了。那如何初始化呢?

最簡單的方式就是使用構造函數。一個替代方案是使用靜態工廠方法,就是在類中提供一個public的靜態方法,通過它來調用私有的構造函數來構造。靜態工廠方法相對於構造函數有多個好處,跟immutable object相關的一點是,使用靜態工廠方法可以在創建對象的時候重用之前創建好的對象。例如我想創建一個immuable object Age類表示年齡。可以使用構造函數new Age(10)。此時我又想再創建一個new String(10)。如果還是這么調用,那么是兩個對象。既然Age是immutable的,那兩個對象其實是浪費空間了。所以一種可選的辦法是創建一個static的Age.create函數。在其中維護一個hash,保存數字到Age對象的映射。然后發現這個數字已經有對應的Age對象,就直接返回之前曾經創建好的對象。

但無論是構造函數還是靜態工廠方法,都有一個問題。如果類內部有多個成員變量,那么在構造函數或靜態工廠方法中需要多個參數。參數太多,方法就很容易用錯。另一個問題時有時候這些參數不方便同時都准備好,需要分階段准備。所以還必須把已經計算好的參數先保存起來供所有參數都准備好后一起使用。此時一種很好的模式是Builder。我們可以為每個復雜的immuable object對象都提供一個builder類。builder類中有跟需要創建的對象相同的成員。builder類有兩類函數,一類是多個setter,每個setter設置一個builder類的成員,然后返回builder;然后是create,調用需要被創建的immuable object的構造函數。需要創建的immuable object需要提供一個以builder為參數的構造函數,把builder類的成員復制到需要創建的對象的成員中以創建對象。builder模式其實是把使用對象和創建對象進行了解耦。builder負責創建,對象本身提供訪問操作供外界使用。在builder中,維護了正在准備中的多個參數,其中還可以對參數進行各種有效性判斷。當然,這種解耦只有在構造足夠復雜或者需要批量產生對象時才產生好的結果,否則只會增加復雜性。

那如果一個對象必須是mutable有什么需要注意的嗎?

  • 在函數傳遞參數時,必須是用保護性拷貝。就是說,無論是使用外界傳入的參數,還是向外界返回結果,如果參數和結果是mutable的,必須拷貝一份再使用或者傳出。這樣子可以保證我這個對象的內部成員是不會因為外界的不經意的或者惡意的操作而發生變化。面向對象的核心之一就是數據的封裝性。一個對象封裝了內部狀態,並公開了一些方法以操作或獲取這些狀態。對狀態的改變只能通過該對象自己的方法,而不能是其他對象的操作的副產品。所以對於mutable object,保護性拷貝是必須的。現實中很多非常難以調試的bug,都是因為一個對象的狀態被遙遠的另外一個對象所改變,這種蝴蝶效應類的bug特別難調試。
  • 在多線程環境中,如果一定要共享mutalbe object,那一般都加鎖來確保同步。所以最好讓mutable object限制在一個線程中使用,讓其計算出的immuable object來共享,盡量減少兩個線程同時讀寫同一個變量的可能性。如果一定要共享mutable object,那比較好的方式是在該對象中進行加鎖等同步操作,而不是讓外界調用者來自己同步,這樣的對象也是線程安全的。

最后想到的是一個例子。在一個Android View中調用addTouchListerner,增加了一個匿名類的listener。在listener中需要訪問外部類的一個成員,那我該如何訪問呢?如果直接訪問,那該成員可能被外部類所改變,那外部類的改變就影響了內部類的狀態,就是mutable了;如果我把這個成員變量保護性拷貝給一個final的局部變量,然后在listener中使用,那在構造listener的賦值之后,該對象的狀態就不再發生變化,所以該對象就是immutable的。當然實際上,我們往往更需要那個mutable的listener,因為它使用的是外部對象的最新狀態。

 

 

 


免責聲明!

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



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