一、面試常談: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類設計為一個不可變的類,有這麼幾種方案:
Instant
、LocalDateTime
或ZonedDateTime
來代替Date使用從Java 8開始,解決此問題的顯而易見的方法是使用
Instant
、LocalDateTime
或ZonedDateTime
來代替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亦提供以下不可變集合: