學Guava發現:不可變特性與防禦性程式設計

localhost02發表於2019-03-07

一、面試常談:String類與不可變特性

問:String類是可變的嗎?

答:emm……由於String類的底層是final關鍵字修飾,因此它是不可變的。

問:它被設計為不可變的好處有哪些呢?

答:

  • 節約記憶體

    大家都知道,程式設計的時候,String類是大量被使用的(試著用VisualVm等工具分析堆,你會發現永遠char[]型別是佔用空間最多的。巧了,String類的底層實現也正是char[])。

    如果像普通物件那樣,每次使用都new一個,恐怕你設定的JVM 堆大小得慎重考慮一下了。

    因此出現了一個叫做常量池的東西,比如String a="abc"String b="abc",那麼a和b都指向常量池的"abc"這個地址。這樣,多個變數,可以共用一個常量池地址,節約了記憶體。

  • 執行緒安全

    常說實現執行緒安全的方法之一就是使用final關鍵字將變數修改為常量,那麼為什麼不可變的常量是執行緒安全的呢?

    很簡單,比如多執行緒併發修改同一變數,如果不加同步進行控制,必然會出現資料不一致問題。但是由於String類是不可變的,根本就不支援你修改,那怎麼可能出現資料不一致問題呢?(感覺像是在扯淡,o(∩_∩)o 哈哈!)

  • 資料安全

    這裡的資料安全,就和下文說道的防禦性程式設計有關係了。

    假設String類可變:

    String name1 = "張三";
    String name2 = name1;
    user.setName(name1);
    name2 = "李四";
    System.out.println(user.getName());
    
    輸出:李四
    複製程式碼

    what?這位使用者明明名字叫張三,咋個無端變成李四了?

  • 提高快取效率

    大家都知道HashMap.put(key,value),需要對key進行hashcode運算。

    hashcode是String型別。因為String的不可變特性,就不需要擔心hashcode值被修改,可以快取起來多次使用,減少hashcode計算次數。

二、進階梳理:不可變特性與防禦性程式設計

有一個Period 類

public final class Period {

    private final Date start;
    private final Date end;

    public Period(Date start, Date end) {
        if (start.compareTo(end) > 0)
            throw new IllegalArgumentException(start + " after " + end);
        this.start = start;
        this.end = end;
    }

    public Date getStart() {
        return start;
    }

    public Date getEnd() {
        return end;
    }
}
複製程式碼

乍一看,這個類似乎是不可變的(即類中的資料不會發生變化)。而事實上,真的是這樣嗎?

Date start = new Date();
Date end = new Date();

Period p = new Period(start, end);

end.setYear(78); 

System.out.println(p.getEnd().toLocalString());

輸出:1978-3-2 18:38:40
複製程式碼

以上程式碼和剛剛的那個“張三、李四”的例子很像。在類例項的外部,直接修改無關變數值,最後導致類例項內部的資料也變化了。

這種情況往往不易被程式設計師在編碼時所發現,從而由於資料的變化導致業務bug。

因此,想要把Period類設計為一個不可變的類,有這麼幾種方案:

  • InstantLocalDateTimeZonedDateTime來代替Date

    使用從Java 8開始,解決此問題的顯而易見的方法是使用InstantLocalDateTimeZonedDateTime來代替Date。因為Instant和其他java.time包下的類是不可變的。Date已過時,不應再在新程式碼中使用

  • Period類重設計
public Period(Date start, Date end) {
	
	//防禦性拷貝:構造一個新Date物件,這樣,這個內部的start變數和外面的那個start變數將沒有任何聯絡
    this.start = new Date(start.getTime());
    this.end   = new Date(end.getTime());

    if (this.start.compareTo(this.end) > 0)
      throw new IllegalArgumentException(this.start + " after " + this.end);
}
複製程式碼

上面提到了一個名詞:“防禦性拷貝”,很確切。除了構造新Date物件,還有深克隆的方式,但是此處不推薦使用克隆。至於為什麼?由於篇幅有限,大家可自行百度!

那麼,這樣就實現了Period類不可變了嗎?

並沒有!由於該類內部的私有資料還提供了getter方法,因此仍然可能通過getter方法修改該類的內部資料。

因此,我們還需要:

 public Date getStart() {
        return new Date(start.getTime());
    }

    public Date getEnd() {
        return new Date(end.getTime());
    }
複製程式碼

這個有點像資料庫中的檢視了,可以給你看,但你不能修改源!


最後總結一下,防禦性程式設計到底是什麼呢?

防禦性程式設計是一種比較泛化的概念,是一種細緻、謹慎的程式設計習慣。

我們在寫程式碼的時候,需要時刻考慮到:程式碼是否正確? 程式碼是否正確? 程式碼是否正確?

例如:

  • 你可以利用不可變特性、構造時拷貝物件等方法來確保一個類的不可變
  • 很多時候,考慮使用防禦性拷貝,避免直接在原始例項上進行操作
  • 接收引數時考慮引數的是否非空等
  • 是否引發效能問題、死鎖問題
  • ……

三、JAVA設計:我感受到的防禦性程式設計

1、String、Integer等的不可變特性

原因上面已經說明了!

2、Arrays.asList返回僅可檢視的“檢視”

Arrays.asList()返回一個ArrayList內部類,沒有add()remove()無法改變長度等,這樣設計的初衷是什麼?為什麼不直接返回可變長的ArrayList(new ArrayList())?

和我們剛剛的重寫getter方法類似,用於保證物件安全不可改變特性!

舉個例子,就是你有一個陣列,怎麼設計一個方法:保證既可以遍歷,又不能修改呢?

返回一個繼承了List介面的輕量級“檢視”不失為一個好的設計方式。而直接返回陣列則是不安全的選擇。

3、不可變集合的各種實現

為什麼需要不可變集合?

不可變物件有很多優點,包括:

  • 當物件被不可信的庫呼叫時,不可變形式是安全的;
  • 不可變物件被多個執行緒呼叫時,不存在競態條件問題
  • 不可變集合不需要考慮變化,因此可以節省時間和空間。所有不可變的集合都比它們的可變形式有更好的記憶體利用率(分析和測試細節);
  • 不可變物件因為有固定不變,可以作為常量來安全使用。
  • 建立物件的不可變拷貝是一項很好的防禦性程式設計技巧。

如果你沒有修改某個集合的需求,或者希望某個集合保持不變時,把它防禦性地拷貝到不可變集合是個很好的實踐。

JDK的實現

JDK的Collections類提供以下不可變集合,用於開發者的一些不可變需求:

學Guava發現:不可變特性與防禦性程式設計

Guava的實現

同時,Guava亦提供以下不可變集合:

學Guava發現:不可變特性與防禦性程式設計

相關文章