Effective Java 第三版——17. 最小化可變性

Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必不少人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到如今已經將近8年的時間,但隨着Java 6,7,8,甚至9的發佈,Java語言發生了深入的變化。
在這裏第一時間翻譯成中文版。供你們學習分享之用。java

Effective Java, Third Edition

17. 最小化可變性

不可變類簡單來講是它的實例不能被修改的類。 包含在每一個實例中的全部信息在對象的生命週期中是固定的,所以不會觀察到任何變化。 Java平臺類庫包含許多不可變的類,包括String類,基本類型包裝類以及BigInteger類和BigDecimal類。 有不少很好的理由:不可變類比可變類更容易設計,實現和使用。 他們不太容易出錯,更安全。程序員

要使一個類不可變,請遵循如下五條規則:數組

  1. 不要提供修改對象狀態的方法(也稱爲mutators)。
  2. 確保這個類不能被繼承。 這能夠防止粗心的或惡意的子類,假設對象的狀態已經改變,從而破壞類的不可變行爲。 防止子類化一般是經過final修飾類,可是咱們稍後將討論另外一種方法。
  3. 把全部屬性設置爲final。經過系統強制執行,清楚地表達了你的意圖。 另外,若是一個新建立的實例的引用從一個線程傳遞到另外一個線程而沒有同步,就必須保證正確的行爲,正如內存模型[JLS,17.5; Goetz06,16]所述。
  4. 把全部的屬性設置爲private。 這能夠防止客戶端得到對屬性引用的可變對象的訪問權限並直接修改這些對象。 雖然技術上容許不可變類具備包含基本類型數值的公共final屬性或對不可變對象的引用,但不建議這樣作,由於它不容許在之後的版本中更改內部表示(項目15和16)。
  5. 確保對任何可變組件的互斥訪問。 若是你的類有任何引用可變對象的屬性,請確保該類的客戶端沒法得到對這些對象的引用。 切勿將這樣的屬性初始化爲客戶端提供的對象引用,或從訪問方法返回屬性。 在構造方法,訪問方法和readObject方法(條目 88)中進行防護性拷貝(條目 50)。

之前條目中的許多示例類都是不可變的。 其中這樣的類是條目 11中的PhoneNumber類,它具備每一個屬性的訪問方法(accessors),但沒有相應的設值方法(mutators)。 這是一個稍微複雜一點的例子:緩存

// Immutable complex number class

public final class Complex {

    private final double re;

    private final double im;

    public Complex(double re, double im) {

        this.re = re;

        this.im = im;

    }

    public double realPart() {

        return re;

    }

    public double imaginaryPart() {

        return im;

    }

    public Complex plus(Complex c) {

        return new Complex(re + c.re, im + c.im);

    }

    public Complex minus(Complex c) {

        return new Complex(re - c.re, im - c.im);

    }

    public Complex times(Complex c) {

        return new Complex(re * c.re - im * c.im,

                re * c.im + im * c.re);

    }

    public Complex dividedBy(Complex c) {

        double tmp = c.re * c.re + c.im * c.im;

        return new Complex((re * c.re + im * c.im) / tmp,

                (im * c.re - re * c.im) / tmp);

    }

    @Override

    public boolean equals(Object o) {

        if (o == this) {

            return true;

        }

        if (!(o instanceof Complex)) {

            return false;

        }

        Complex c = (Complex) o;

        // See page 47 to find out why we use compare instead of ==

        return Double.compare(c.re, re) == 0

                && Double.compare(c.im, im) == 0;

    }

    @Override

    public int hashCode() {

        return 31 * Double.hashCode(re) + Double.hashCode(im);

    }

    @Override

    public String toString() {

        return "(" + re + " + " + im + "i)";

    }
}

這個類表明了一個複數(包含實部和虛部的數字)。 除了標準的Object方法以外,它還爲實部和虛部提供訪問方法,並提供四個基本的算術運算:加法,減法,乘法和除法。 注意算術運算如何建立並返回一個新的Complex實例,而不是修改這個實例。 這種模式被稱爲函數式方法,由於方法返回將操做數應用於函數的結果,而不修改它們。 與其對應的過程(procedural)或命令(imperative)的方法相對比,在這種方法中,將一個過程做用在操做數上,致使其狀態改變。 請注意,方法名稱是介詞(如plus)而不是動詞(如add)。 這強調了方法不會改變對象的值的事實。 BigIntegerBigDecimal類沒有遵照這個命名約定,並致使許多使用錯誤。安全

若是你不熟悉函數式方法,可能會顯得不天然,但它具備不變性,具備許多優勢。 不可變對象很簡單。 一個不可變的對象能夠徹底處於一種狀態,也就是被建立時的狀態。 若是確保全部的構造方法都創建了類不變量,那麼就保證這些不變量在任什麼時候候都保持不變,使用此類的程序員無需再作額外的工做。 另外一方面,可變對象能夠具備任意複雜的狀態空間。 若是文檔沒有提供由設置(mutator)方法執行的狀態轉換的精確描述,那麼可靠地使用可變類多是困難的或不可能的。ide

不可變對象本質上是線程安全的; 它們不須要同步。 被多個線程同時訪問它們時並不會被破壞。 這是實現線程安全的最簡單方法。 因爲沒有線程能夠觀察到另外一個線程對不可變對象的影響,因此不可變對象能夠被自由地共享。 所以,不可變類應鼓勵客戶端儘量重用現有的實例。 一個簡單的方法是爲經常使用的值提供公共的靜態 final常量。 例如,Complex類可能提供這些常量:函數

public static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE  = new Complex(1, 0);
public static final Complex I    = new Complex(0, 1);

這種方法能夠更進一步。 一個不可變的類能夠提供靜態的工廠(條目 1)來緩存常常被請求的實例,以免在現有的實例中建立新的實例。 全部基本類型的包裝類和BigInteger類都是這樣作的。 使用這樣的靜態工廠會使客戶端共享實例而不是建立新實例,從而減小內存佔用和垃圾回收成本。 在設計新類時,選擇靜態工廠代替公共構造方法,能夠在之後增長緩存的靈活性,而不須要修改客戶端。性能

不可變對象能夠自由分享的結果是,你永遠不須要作出防護性拷貝( defensive copies)(條目 50)。 事實上,永遠不須要作任何拷貝,由於這些拷貝永遠等於原始對象。 所以,你不須要也不該該在一個不可變的類上提供一個clone方法或拷貝構造方法(copy constructor)(條目 13)。 這一點在Java平臺的早期階段還不是很好理解,因此String類有一個拷貝構造方法,可是它應該儘可能不多使用(條目 6)。學習

不只能夠共享不可變的對象,並且能夠共享內部信息。 例如,BigInteger類在內部使用符號數值表示法。 符號用int值表示,數值用int數組表示。 negate方法生成了一個數值相同但符號相反的新BigInteger實例。 即便它是可變的,也不須要複製數組;新建立的BigInteger指向與原始相同的內部數組。ui

不可變對象爲其餘對象提供了很好的構件(building blocks),不管是可變的仍是不可變的。 若是知道一個複雜組件的內部對象不會發生改變,那麼維護複雜對象的不變量就容易多了。這一原則的特例是,不可變對象能夠構成Map對象的鍵和Set的元素,一旦不可變對象做爲Map的鍵或Set裏的元素,即便破壞了MapSet的不可變性,但不用擔憂它們的值會發生變化。

不可變對象提供了免費的原子失敗機制(條目 76)。它們的狀態永遠不會改變,因此不可能出現臨時的不一致。

不可變類的主要缺點是對於每一個不一樣的值都須要一個單獨的對象。 建立這些對象可能代價很高,特別是若是是大型的對象下。 例如,假設你有一個百萬位的BigInteger    ,你想改變它的低位:

BigInteger moby = ...;

moby = moby.flipBit(0);

flipBit方法建立一個新的BigInteger實例,也是一百萬位長,與原始位置只有一位不一樣。 該操做須要與BigInteger大小成比例的時間和空間。 將其與java.util.BitSet對比。 像BigInteger同樣,BitSet表示一個任意長度的位序列,但與BigInteger不一樣,BitSet是可變的。 BitSet類提供了一種方法,容許你在固定時間內更改百萬位實例中單個位的狀態:

BitSet moby = ...;

moby.flip(0);

若是執行一個多步操做,在每一步生成一個新對象,除最終結果以外丟棄全部對象,則性能問題會被放大。這裏有兩種方式來處理這個問題。第一種辦法,先猜想一下會常常用到哪些多步的操做,而後講它們做爲基本類型提供。若是一個多步操做是做爲一個基本類型提供的,那麼不可變類就沒必要在每一步建立一個獨立的對象。在內部,不可變的類能夠是任意靈活的。 例如,BigInteger有一個包級私有的可變的「夥伴類(companion class)」,它用來加速多步操做,好比模冪運算( modular exponentiation)。出於前面所述的全部緣由,使用可變夥伴類比使用BigInteger要困可貴多。 幸運的是,你沒必要使用它:BigInteger類的實現者爲你作了不少努力。

若是你能夠準確預測客戶端要在你的不可變類上執行哪些複雜的操做,那麼包級私有可變夥伴類的方式能夠正常工做。若是不是的話,那麼最好的辦法就是提供一個公開的可變夥伴類。 這種方法在Java平臺類庫中的主要例子是String類,它的可變夥伴類是StringBuilder(及其過期的前身StringBuffer類)。

如今你已經知道如何建立一個不可改變類,而且瞭解不變性的優勢和缺點,下面咱們來討論幾個設計方案。 回想一下,爲了保證不變性,一個類不得容許子類化。 這能夠經過使類用 final 修飾,可是還有另一個更靈活的選擇。 而不是使不可變類設置爲 final,可使其全部的構造方法私有或包級私有,並添加公共靜態工廠,而不是公共構造方法(條目 1)。 爲了具體說明這種方法,下面以Complex爲例,看看如何使用這種方法:

// Immutable class with static factories instead of constructors

public class Complex {

    private final double re;

    private final double im;

    private Complex(double re, double im) {

        [this.re](http://this.re) = re;

        [this.im](http://this.im) = im;

    }

    public static Complex valueOf(double re, double im) {

        return new Complex(re, im);

    }

    ... // Remainder unchanged

}

這種方法每每是最好的選擇。 這是最靈活的,由於它容許使用多個包級私有實現類。 對於駐留在包以外的客戶端,不可變類其實是final的,由於不可能繼承來自另外一個包的類,而且缺乏公共或受保護的構造方法。 除了容許多個實現類的靈活性之外,這種方法還能夠經過改進靜態工廠的對象緩存功能來調整後續版本中類的性能。

BigIntegerBigDecimal被寫入時,不可變類必須是有效的final,所以它們的全部方法均可能被重寫。不幸的是,在保持向後兼容性的同時,這一事實沒法糾正。若是你編寫一個安全性取決於來自不受信任的客戶端的BigInteger或BigDecimal參數的不變類時,則必須檢查該參數是「真實的」BigInteger仍是BigDecimal,而不該該是不受信任的子類的實例。若是是後者,則必須在假設多是可變的狀況下保護性拷貝(defensively copy)(條目 50):

public static BigInteger safeInstance(BigInteger val) {

    return val.getClass() == BigInteger.class ?
            val : new BigInteger(val.toByteArray());
}

在本條目開頭關於不可變類的規則說明,沒有方法能夠修改對象,而且它的全部屬性必須是final的。事實上,這些規則比實際須要的要強硬一些,其實能夠有所放鬆來提升性能。 事實上,任何方法都不能在對象的狀態中產生外部可見的變化。 然而,一些不可變類具備一個或多個非final屬性,在第一次須要時將開銷昂貴的計算結果緩存在這些屬性中。 若是再次請求相同的值,則返回緩存的值,從而節省了從新計算的成本。 這個技巧的做用偏偏是由於對象是不可變的,這保證了若是重複的話,計算會獲得相同的結果。

例如,PhoneNumber類的hashCode方法(第53頁的條目 11)在第一次調用改方法時計算哈希碼,並在再次調用時對其進行緩存。 這種延遲初始化(條目 83)的一個例子,String類也使用到了。

關於序列化應該加上一個警告。 若是你選擇使您的不可變類實現Serializable接口,而且它包含一個或多個引用可變對象的屬性,則必須提供顯式的readObjectreadResolve方法,或者使用ObjectOutputStream.writeUnsharedObjectInputStream.readUnshared方法,即默認的序列化形式也是能夠接受的。 不然攻擊者可能會建立一個可變的類的實例。 這個主題會在條目 88中會詳細介紹。

總而言之,堅定不要爲每一個屬性編寫一個get方法後再編寫一個對應的set方法。 除非有充分的理由使類成爲可變類,不然類應該是不可變的。 不可變類提供了許多優勢,惟一的缺點是在某些狀況下可能會出現性能問題。 你應該始終使用較小的值對象(如PhoneNumberComplex),使其不可變。 (Java平臺類庫中有幾個類,如java.util.Datejava.awt.Point,本應該是不可變的,但實際上並非)。你應該認真考慮建立更大的值對象,例如StringBigInteger ,設成不可改變的。 只有當你確認有必要實現使人滿意的性能(條目 67)時,才應該爲不可改變類提供一個公開的可變夥伴類。

對於一些類來講,不變性是不切實際的。若是一個類不能設計爲不可變類,那麼也要儘量地限制它的可變性。減小對象能夠存在的狀態數量,能夠更容易地分析對象,以及下降出錯的可能性。所以,除非有足夠的理由把屬性設置爲非 final 的狀況下,不然應該每一個屬性都設置爲 final 的。把本條目的建議與條目15的建議結合起來,你天然的傾向就是:除非有充分的理由不這樣作,不然應該把每一個屬性聲明爲私有final的

構造方法應該建立徹底初始化的對象,並創建全部的不變性。 除非有使人信服的理由,不然不要提供獨立於構造方法或靜態工廠的公共初始化方法。 一樣,不要提供一個「reinitialize」方法,使對象能夠被重用,就好像它是用不一樣的初始狀態構建的。 這樣的方法一般以增長的複雜度爲代價,僅僅提供不多的性能優點。

CountDownLatch類是這些原理的例證。 它是可變的,但它的狀態空間有意保持最小範圍內。 建立一個實例,使用它一次,並完成:一旦countdown鎖的計數器已經達到零,不能再重用它。

在這個條目中,應該添加關於Complex類的最後一個註釋。 這個例子只是爲了說明不變性。 這不是一個工業強度複雜的複數實現。 它對複數使用了乘法和除法的標準公式,這些公式不正確會進行不正確的四捨五入,沒有爲複數的NaN和無窮大提供良好的語義[Kahan91,Smith62,Thomas94]。

相關文章
相關標籤/搜索