1. 程式人生 > >Effective Java讀書筆記-使可變性最小化

Effective Java讀書筆記-使可變性最小化

存在不可變類的原因:不可變的類比可變的類更加易於設計、實現和使用。它們不容易出錯且更加安全。

使類變為不可變需要遵守的五項原則:

  1. 不要提供任何會修改物件狀態的方法。
  2. 保證類不會被拓展。這樣可以防止粗心或者惡意的子類假裝物件的狀態已經改變,從而破壞類的不可變行為。為了防止子類化,一般的做法是使類變為final的。
  3. 使所有的域都是final的。
  4. 使所有的域都變為私有的。
  5. 確保任何可變元件的互斥訪問。如果類具有指向可變物件的域,就必須確保該類的客戶端無法獲得指向這些物件的引用。並且,永遠不要用客戶端提供的物件引用來初始化這樣的域,也不要從任何訪問方法中返回該物件的引用。在構造器、訪問方法和readObject方法中請使用保護性拷貝技術

不可變物件比較簡單。不可變物件可以只有一種狀態,即被建立時的狀態。如果能夠確保所有的構造器都建立了這個類的約束關係,就可以確保這些約束關係在整個類的生命週期中不再發生任何變化,你和使用這個類的程式設計師都無需進行任何動作來維護這些約束關係。

不可變物件本質上是執行緒安全的,它們不要求同步。不可變物件可以自由的被共享。不可變物件應當充分的利用這一優勢,對於頻繁應用的值,為它們提供靜態的final常量。例如:

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

不可變的類可以提供一些靜態工廠,它們把頻繁被請求的例項快取起來,從而當現有例項可以符合請求的時候,就不必建立新的例項。所有基本型別的包裝類和BigInteger都有這樣的靜態工廠。使用這樣的靜態工廠也使得客戶端之間可以共享現有的例項,而不用建立新的例項,從而降低記憶體的佔用和垃圾回收的成本。
在設計新的類的時候,選擇使用靜態工廠代替公有的構造器可以讓你以後有新增快取的靈活性,而不必影響客戶端。

不要給不可變的類提供拷貝:

“不可變的物件可以被自由的共享”導致的結果是,永遠不需要進行保護性拷貝。實際上根本不需要做任何拷貝,因為這些拷貝始終等於原物件。因此不需要也不應該為不可變的物件提供clone方法或者拷貝構造器。

這一點在Java的早期並不好理解,所以String類仍然具有拷貝構造器。

不僅可以共享不可變的物件,甚至可以共享它們的內部資訊。
不可變物件為其它物件提供了大量的構件。不可變物件構成了大量的對映鍵和集合元素,一旦不可變物件進入到對映或者集合中,儘管破壞了對映或者集合的不變性約束,但是也不用擔心它們的值會發生變化。

不可變類的缺點:

對每一個不同的值都需要一個單獨的物件。建立這種物件的代價可能很高,特別對於大型物件的情形。
如果執行一個多步驟的操作,並且每個步驟都會產生一個新的物件,除了最後的結果之外其它的物件最後都會被丟棄,此時效能問題就顯露出來了。處理這種問題有兩種方法:

  • 猜測一下經常會用到的多步驟操作,然後將它們作為基本型別提供。如果某個多步驟操作已經作為基本型別提供了,不可變類就不必在每個步驟單獨建立一個物件,不可變的類在內部可以更加的靈活。
  • 如果能夠精準的預測出客戶端將在不可變的類上執行那些複雜的多階段操作,這種包級私有的可變配套類方法就可以工作的更好。如果無法預測,最好的辦法就是提供一個公有的可變配套類。在Java平臺類庫中,這種方法的主要例子是String類,它的可變配套類是StringBuilder類。

確保類不變性的方法:

為了使類保持不變性,類不允許自身被子類化。除了使用final方法之外,還有一種更為靈活的方法可以做到這一點,就是讓類的構造器變為私有的或者是包級私有的,新增靜態工廠來代替公有的構造器。例如:

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

雖然這種方法不常用,但卻是最好的替代方法。它是靈活的,因為允許多個包級私有類的實現。除了允許多個實現類的靈活性之外,這種方法還能夠改善靜態工廠的物件快取能力,在後續的發行版本中改進該類的效能。

如果你在編寫一個類的時候,它的安全性依賴於BigInteger或BigDecimal引數的不可變性,就必須進行檢查,以確定這個引數是否為真正的BigInteger或BigDecimal,而不是不可信任子類的例項。如果是後者的話,就必須假設它可能是可變的前提下對它進行拷貝性保護。

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

不可變類的好處:

沒有方法會修改物件,並且它的所有域都必須是final的。沒有一個方法能夠對物件的狀態產生外部可見的改變。許多的不可變的類擁有一個或多個非final的域,它們在第一次被請求執行這些計算的時候,把一些開銷昂貴的計算結果快取在這些域中。如果將來再次請求同樣的計算,就直接返回這些快取的值,從而節約了重新計算所需要的開銷。這種技巧可以很好的工作,因為物件是不可變的。它的不可變確保了這些計算如果再次被執行,就會產生同樣的效果。

延遲初始化:

類的物件在第一次呼叫的時候,計算出相應的值,之後把它快取起來,以備將來再次被呼叫的時候使用。

序列化時應當注意的問題:

如果你選擇讓自己不可變的類實現Serializable介面,並且包含一個或者多個指向可變物件的資料域,就必須提供一個顯式的readObject或者readResolve方法,或者使用ObjectOutputStream.writeUnshared和ObjectInputStream.readUnshared方法。即使預設的序列化形式是可以接受的,也是如此。否則攻擊者可能從不可變的類建立可變的例項。

注意的問題:

  • 總之,不要為每一個get方法編寫一個相應的set方法。除非有很好的理由讓類成為可變的類,否則就應該是不可變的。不可變的類有很多的優點,唯一的缺點是在特定的情況下存在潛在的效能問題。
  • 如果類不能被做成是不可變的,仍然應該儘可能的限制它的可變性。降低物件可以存在的狀態數,可以更容易的分析該物件的行為,同時降低出錯的可能性。除非有令人信服的理由要使域變成非final的,否則要使每個域都是final的。
  • 構造器應該建立完全初始化的物件,並建立起所有的約束關係。不要再構造器或者靜態工廠之外再提供公有的初始化方法,除非有令人信服的理由必須這麼做。同樣的也不應該提供“重新初始化”的方法。“重新初始化”的方法通常沒有帶來太多的效能優勢。