Effective-java 3 中文翻譯系列 (Item 17 使可變性最小化)

原文鏈接

文章也上傳到的

github

(歡迎關(guān)注,歡迎大神提點(diǎn)。)


ITEM 17 使可變性最小化


一個不可變的類指的是一個類的對象不能被修改。在這個對象的生命周期中它所包含的信息是不變的。Java中有很多不可變的類,比如String、原始類型、BigInteger、BigDecimal。不可變類比可變類更容易設(shè)計、實(shí)現(xiàn)和使用,而且更少出錯也更安全。那么怎樣才能使類變得不可變呢?請遵循以下5條原則:

  • 不提供修改類對象的方法。
  • 類不能被繼承。這樣可以防止惡意或者粗心的子類改變其不可變性。一般會用final修飾類防止被子類修改,但是我們還有一另一種替代方案會在后面進(jìn)行討論。
  • 將所有屬性設(shè)置成final。使用這個系統(tǒng)的強(qiáng)制語法不但清晰的表達(dá)了你的意圖,而且可以防止在不同線程中訪問同一個對象出現(xiàn)的不同行為問題(例如讀寫問題)。
  • 將所有屬性設(shè)置成private。這樣可以防止可變對象被調(diào)用者獲取并修改。雖然在不可變的類中可以將一個public final 的屬性設(shè)置一個初始(默認(rèn)的)值或者指向一個不可變的對象,但是這是是不推薦的做法,以為這樣在后續(xù)的代碼中我們就沒辦去再次修改這個值了,因?yàn)槠涫莊inal的。所以我們建議不設(shè)置初始值,除非你真的有需要這么做。
  • 確保任何可變對象都只能被自己訪問。如果你的類中有任何引用可變對象的屬性,確保它不能被調(diào)用者獲取到。不要在accessor(訪問器)方法中返回這樣的屬性。在構(gòu)造器、accessors和readObject(Item 88)方法中進(jìn)行防御性拷貝(Item 50)。

前面的很多例子中的類都是不可變的,例如Item 11中的PhoneNumber,它的每個屬性都可以被訪問但不能被修改。這里有一個更加完整的例子:

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)";
    }
}

這個類表示一個復(fù)數(shù)(有實(shí)部和虛部的數(shù)字)。除了一些標(biāo)準(zhǔn)的對象方法之外,它提供了訪問實(shí)部和虛部的方法以及基礎(chǔ)的加、減、乘、除方法。請注意:這些算數(shù)運(yùn)算的返回結(jié)果中并沒有修改這個對象的屬性,而是生成一個新的Complex實(shí)例對象。這種模式被稱為函數(shù)式方法,因?yàn)檫@種方法把調(diào)用函數(shù)的結(jié)果在不修改函數(shù)調(diào)用者的同時返回給調(diào)用者。注意方法名是介詞(如plus)而不是動詞(如add),是為了強(qiáng)調(diào)方法不會修改對象的值。在BigInteger和BigDecimal中沒有遵循這種命名規(guī)范,所以造成了很多錯誤的調(diào)用。

如果你不熟悉函數(shù)式方法,可能會覺得它看著不太自然,但是它具有不可變性而且有很多優(yōu)點(diǎn)。使對象不可變。如果你能確認(rèn)類的所有構(gòu)造方法都不改變其狀態(tài),并且無論何時都不會被改變,那么一個不可變對象的狀態(tài)就可以一直被保持在它被創(chuàng)建時的狀態(tài)。因?yàn)榭勺儗ο缶哂锌勺冃?,如果在setter方法中沒有給出詳細(xì)說明其變化的描述,那么這個可變的類將很難甚至不能被可靠的使用。

不可變對象與生俱來就是線程安全的,所以不需要synchronization。它們不會被多個線程同時訪問所污染,所以這是實(shí)現(xiàn)線程安全最簡單的方法。不可變對象可以自由分享。不可變的類應(yīng)該鼓勵調(diào)用者盡可能重用同一個類對象。簡單有效的方法是提供public static 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);

這種方法還可以更進(jìn)一步。一個不可變的類可以提供static 的工廠(Item 1)來緩存經(jīng)常要訪問的對象,以避免當(dāng)一個對象存在了還會被經(jīng)常的創(chuàng)建。現(xiàn)在的基礎(chǔ)類型的包裝類和BigInteger都是這么做的。這樣帶來的好處是:避免創(chuàng)建多余的相同對象,減少內(nèi)存消耗和垃圾回收消耗。使用static工廠而不是public的構(gòu)造方法是為了在將來想添加一個內(nèi)容時提供靈活性,不用修改調(diào)用者的代碼。

一個不可變對象可以被自由分享,這就意味著不需要對他們進(jìn)行防御性拷貝(Item50)。因?yàn)樗鼈兊膬?nèi)容永遠(yuǎn)和初始時是一樣的。所以不可變的類中你不需要提供clone方法或者copy構(gòu)造方法(Item13)。這在Java早期的時候是不太好理解的,所以在String類中仍然有copy的構(gòu)造方法,但是盡量應(yīng)該不要這么使用(Item6)。

不僅可以公開不可變對象,而且可以公開它們的實(shí)現(xiàn)。例如:BigInteger類使用一個有符號的數(shù),符號用一個int類型表示,值用int數(shù)組表示。negate方法返回一個符號相反絕對值相等的新的BigInteger對象。即使它是可變的也不需要使用copy數(shù)組;這個新的對象指向相同的內(nèi)部數(shù)組。

不可變對象作為其他對象的組成元素很有優(yōu)勢,無論這個對象可不可變,因?yàn)椴挥脫?dān)心不可變量被胡亂修改。

不可變對象免費(fèi)提供失敗原子性(Item76)。它們的狀態(tài)不會被改變,所以不可能不一致。

不可變對象的主要缺點(diǎn)是:對于不同的值它都需要一個全新的對象。尤其是對于較大的對象,創(chuàng)建出來的代價是高昂的。例如:你有一個百萬為的BigInteger對象,當(dāng)你想改變它的最低位時:

BigInteger moby = ...;
moby = moby.flipBit(0);

方法flipBit會創(chuàng)建一個新的百萬位的BigInteger對象出來,不同的僅僅是最后一位。這個操作消耗的時間和空間和BigInteger的大小成正比。對比java.util.BitSet和BigInteger,BitSet是可變的,有任意長度的bit位,提供僅改變數(shù)值中一位的方法:

BitSet moby = ...;
moby.flip(0);

如果你執(zhí)行很多步驟,每一步都產(chǎn)生一個新對象,并僅僅在最后才釋放所有對象的話,這將會產(chǎn)生很嚴(yán)重的性能問題。有兩種方法應(yīng)對這個問題。第一是將公用的多步操作封裝起來,就不用每一步都生成一個新的對象了。例如BigInteger類有一個包私有的可變“伙伴類”,用來加速類似于模冪運(yùn)算的多步操作。

如果你能預(yù)測調(diào)用者可能用你的不可變類做的哪些操作,就可以將這些操作做成包私有級別的方法。如果沒辦法預(yù)測可能需要什么操作時,你最好提供一個public的可變伙伴類,例如String類,它的可變伙伴類是StringBuilder(它的已廢棄的前身是StringBuffer)。

現(xiàn)在你已經(jīng)知道了如何創(chuàng)建一個不可變的類,而且知道了不可變性的優(yōu)點(diǎn)和缺點(diǎn),讓我們導(dǎo)論一下設(shè)計上的替代品。會想一下為了保證不可變性,一個類不能被子類化,這可以通過final來實(shí)現(xiàn)。但是有另一個更加靈活的的方案:不是使不可變的類被final修飾,而是使構(gòu)造方法變成private或者package-private的,然后添加public static工廠方法代替public 構(gòu)造方法(Item 1)。請看下面實(shí)例:

//用不可變類的靜態(tài)工廠代替構(gòu)造方法
public class Complex {
    
    private final double re;
    private final double im;
    
    private Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }
    
    public static Complex valueOf(double re, double im) {
        return new Complex(re, im);
    }
    ... // Remainder unchanged
}

通常這種方法是最好的替代品,具有最靈活的特性,因?yàn)檫@樣可以實(shí)現(xiàn)多包級私有特性。包外的類沒辦法繼承這個類,因?yàn)閷τ谄渌赃@個類就是final的,不能繼承不同包的類而且也沒有public或者protected的構(gòu)造方法。這個方法還能在將來的版本中通過優(yōu)化靜態(tài)方法中的對象緩存來優(yōu)化性能。

在BigInteger和BigDecimal類被創(chuàng)造的時候,不可變類必須保證final這個觀點(diǎn)并沒有被普遍的認(rèn)知,所以它們的方法都是有可能被重寫的。不幸的是,這種錯誤現(xiàn)在并不能被修改,因?yàn)樗鼈儸F(xiàn)在都要做向后兼容。如何你依賴于BigInteger和BigDecimal的不可變性實(shí)現(xiàn)一個安全的類時,一定要確認(rèn)它們是真實(shí)的BigInteger和BigDecimal,而不是它們的(不被我們信任的)子類。如果確認(rèn)它們是子類,那么應(yīng)該做防御性拷貝,因?yàn)樗鼈兛赡芤呀?jīng)重寫了實(shí)現(xiàn)(Item50):

public static BigInteger safeInstance(BigInteger val) {
    return val.getClass() == BigInteger.class ?
        val : new BigInteger(val.toByteArray());
}

文章開始時我們說不可變類是不能修改它的對象并且他的屬性都是final的。事實(shí)上,這些規(guī)則有點(diǎn)過于嚴(yán)格了,可以適當(dāng)放松來提高性能。實(shí)際上,沒有方法能提供外部可見的內(nèi)部變化。然而,一些不可變的類有一個或多個非final的屬性,用來緩存一些結(jié)果,這些結(jié)果在第一次被需要的時候會消耗一些計算成本,但是在之后被需要的時候就不需要再次計算,緩存可以直接返回不可變的對象,這保證了重復(fù)計算時返回相同的結(jié)果。

例如PhoneNumber的hashCode方法(Item11),第一次計算的時候會緩存起來,下次就可以直接被使用。同樣的延遲初始的方法(Item83)String類也使用了。

特別注意當(dāng)你的不可變類實(shí)現(xiàn)Serializable接口時,并且如果它包含一個或多個可變的對象,即使默認(rèn)serialized可用,你也必須提供一個清晰的readObject或readResolve方法,或者使用ObjectOutputStream.writeUnshared 和ObjectInputStream.readUnshar

ed方法。否則攻擊者可能會用你的類創(chuàng)建一個可變對象,這種情況在Item88中會詳細(xì)說明。

在不需要的情況下,不要為每一個getter方法寫一個setter方法,如果類可以做成不可變的,就盡量不要使它們可變。不可變的類有很多的優(yōu)點(diǎn),只有一個缺點(diǎn)是在某些情況下可能有性能問題。Java平臺有幾個類本應(yīng)該是不可變但實(shí)際上卻不是,如java.util.Date和java.awt.Point。只有在你必須實(shí)現(xiàn)令人滿意的性能的時候才應(yīng)該把不可變類變成可變的公共類(Item 67)。

不能變成不可變的類,那就應(yīng)該盡量限制它的可變形。減少狀態(tài)的數(shù)量就能使分析問題更加容易,也能減少可能的錯誤。應(yīng)該把變量設(shè)置成final 的,除非有足夠的原因需要設(shè)置成非final的。結(jié)合item15說的,你應(yīng)該自然的把變量設(shè)置成private final的,除非有原因需要設(shè)置成其他的。

構(gòu)造方法應(yīng)該創(chuàng)建完全初始化的對象,以使它們的不確定性確立。除非有充足的理由,都不要創(chuàng)建除了構(gòu)造器和靜態(tài)方法之外的公共初始化方法。同樣,也不要提供重新初始化方法重新把一個對象初始化成新的狀態(tài)。因?yàn)檫@樣增加了復(fù)雜度換來了僅僅一點(diǎn)性能的優(yōu)勢。

類CountDownLatch 遵循了這個規(guī)則。它是可變的但是它的狀態(tài)空間被限制的很小。你創(chuàng)建一個對象,使用它的時候:一旦countdown 的鎖計數(shù)降到0,你是不能重新使用它的。

最后需要提一下這里的Complex類不是一個工業(yè)標(biāo)準(zhǔn)的類,僅僅是為了說明不可變性存在的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容