Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必很多人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到現在已經將近8年的時間,但隨着Java 6,7,8,甚至9的發布,Java語言發生了深刻的變化。
在這里第一時間翻譯成中文版。供大家學習分享之用。
17. 最小化可變性
不可變類簡單來說是它的實例不能被修改的類。 包含在每個實例中的所有信息在對象的生命周期中是固定的,因此不會觀察到任何變化。 Java平台類庫包含許多不可變的類,包括String類,基本類型包裝類以及BigInteger類和BigDecimal類。 有很多很好的理由:不可變類比可變類更容易設計,實現和使用。 他們不太容易出錯,更安全。
要使一個類不可變,請遵循以下五條規則:
- 不要提供修改對象狀態的方法(也稱為mutators)。
- 確保這個類不能被繼承。 這可以防止粗心的或惡意的子類,假設對象的狀態已經改變,從而破壞類的不可變行為。 防止子類化通常是通過final修飾類,但是我們稍后將討論另一種方法。
- 把所有屬性設置為final。通過系統強制執行,清楚地表達了你的意圖。 另外,如果一個新創建的實例的引用從一個線程傳遞到另一個線程而沒有同步,就必須保證正確的行為,正如內存模型[JLS,17.5; Goetz06,16]所述。
- 把所有的屬性設置為private。 這可以防止客戶端獲得對屬性引用的可變對象的訪問權限並直接修改這些對象。 雖然技術上允許不可變類具有包含基本類型數值的公共final屬性或對不可變對象的引用,但不建議這樣做,因為它不允許在以后的版本中更改內部表示(項目15和16)。
- 確保對任何可變組件的互斥訪問。 如果你的類有任何引用可變對象的屬性,請確保該類的客戶端無法獲得對這些對象的引用。 切勿將這樣的屬性初始化為客戶端提供的對象引用,或從訪問方法返回屬性。 在構造方法,訪問方法和
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)。 這強調了方法不會改變對象的值的事實。 BigInteger
和BigDecimal
類沒有遵守這個命名約定,並導致許多使用錯誤。
如果你不熟悉函數式方法,可能會顯得不自然,但它具有不變性,具有許多優點。 不可變對象很簡單。 一個不可變的對象可以完全處於一種狀態,也就是被創建時的狀態。 如果確保所有的構造方法都建立了類不變量,那么就保證這些不變量在任何時候都保持不變,使用此類的程序員無需再做額外的工作。 另一方面,可變對象可以具有任意復雜的狀態空間。 如果文檔沒有提供由設置(mutator)方法執行的狀態轉換的精確描述,那么可靠地使用可變類可能是困難的或不可能的。
不可變對象本質上是線程安全的; 它們不需要同步。 被多個線程同時訪問它們時並不會被破壞。 這是實現線程安全的最簡單方法。 由於沒有線程可以觀察到另一個線程對不可變對象的影響,所以不可變對象可以被自由地共享。 因此,不可變類應鼓勵客戶端盡可能重用現有的實例。 一個簡單的方法是為常用的值提供公共的靜態 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
指向與原始相同的內部數組。
不可變對象為其他對象提供了很好的構件(building blocks),無論是可變的還是不可變的。 如果知道一個復雜組件的內部對象不會發生改變,那么維護復雜對象的不變量就容易多了。這一原則的特例是,不可變對象可以構成Map
對象的鍵和Set
的元素,一旦不可變對象作為Map
的鍵或Set
里的元素,即使破壞了Map
和Set
的不可變性,但不用擔心它們的值會發生變化。
不可變對象提供了免費的原子失敗機制(條目 76)。它們的狀態永遠不會改變,所以不可能出現臨時的不一致。
不可變類的主要缺點是對於每個不同的值都需要一個單獨的對象。 創建這些對象可能代價很高,特別是如果是大型的對象下。 例如,假設你有一個百萬位的BigInteger
,你想改變它的低位:
BigInteger moby = ...;
moby = moby.flipBit(0);
flipBit
方法創建一個新的BigInteger
實例,也是一百萬位長,與原始位置只有一位不同。 該操作需要與BigInteger
大小成比例的時間和空間。 將其與java.util.BitSet
對比。 像BigIntege
r一樣,BitSet
表示一個任意長度的位序列,但與BigInteger
不同,BitSe
t是可變的。 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的,因為不可能繼承來自另一個包的類,並且缺少公共或受保護的構造方法。 除了允許多個實現類的靈活性以外,這種方法還可以通過改進靜態工廠的對象緩存功能來調整后續版本中類的性能。
當BigInteger
和BigDecimal
被寫入時,不可變類必須是有效的final,因此它們的所有方法都可能被重寫。不幸的是,在保持向后兼容性的同時,這一事實無法糾正。如果你編寫一個安全性取決於來自不受信任的客戶端的BigIntege
r或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接口,並且它包含一個或多個引用可變對象的屬性,則必須提供顯式的readObject
或readResolve
方法,或者使用ObjectOutputStream.writeUnshared
和ObjectInputStream.readUnshared
方法,即默認的序列化形式也是可以接受的。 否則攻擊者可能會創建一個可變的類的實例。 這個主題會在條目 88中會詳細介紹。
總而言之,堅決不要為每個屬性編寫一個get方法后再編寫一個對應的set方法。 除非有充分的理由使類成為可變類,否則類應該是不可變的。 不可變類提供了許多優點,唯一的缺點是在某些情況下可能會出現性能問題。 你應該始終使用較小的值對象(如PhoneNumber
和Complex
),使其不可變。 (Java平台類庫中有幾個類,如java.util.Date
和java.awt.Point
,本應該是不可變的,但實際上並不是)。你應該認真考慮創建更大的值對象,例如String
和BigInteger
,設成不可改變的。 只有當你確認有必要實現令人滿意的性能(條目 67)時,才應該為不可改變類提供一個公開的可變伙伴類。
對於一些類來說,不變性是不切實際的。如果一個類不能設計為不可變類,那么也要盡可能地限制它的可變性。減少對象可以存在的狀態數量,可以更容易地分析對象,以及降低出錯的可能性。因此,除非有足夠的理由把屬性設置為非 final 的情況下,否則應該每個屬性都設置為 final 的。把本條目的建議與條目15的建議結合起來,你自然的傾向就是:除非有充分的理由不這樣做,否則應該把每個屬性聲明為私有final的。
構造方法應該創建完全初始化的對象,並建立所有的不變性。 除非有令人信服的理由,否則不要提供獨立於構造方法或靜態工廠的公共初始化方法。 同樣,不要提供一個“reinitialize”方法,使對象可以被重用,就好像它是用不同的初始狀態構建的。 這樣的方法通常以增加的復雜度為代價,僅僅提供很少的性能優勢。
CountDownLatch
類是這些原理的例證。 它是可變的,但它的狀態空間有意保持最小范圍內。 創建一個實例,使用它一次,並完成:一旦countdown鎖的計數器已經達到零,不能再重用它。
在這個條目中,應該添加關於Complex
類的最后一個注釋。 這個例子只是為了說明不變性。 這不是一個工業強度復雜的復數實現。 它對復數使用了乘法和除法的標准公式,這些公式不正確會進行不正確的四舍五入,沒有為復數的NaN和無窮大提供良好的語義[Kahan91,Smith62,Thomas94]。