問:String類是可變的嗎?java
答: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
複製代碼
以上代碼和剛剛的那個「張3、李四」的例子很像。在類實例的外部,直接修改無關變量值
,最後致使類實例內部的數據也變化了。
這種狀況每每不易被程序員在編碼時所發現,從而因爲數據的變化致使業務bug。
所以,想要把Period類設計爲一個不可變的類,有這麼幾種方案:
Instant
、LocalDateTime
或ZonedDateTime
來代替Date
使用從Java 8開始,解決此問題的顯而易見的方法是使用
Instant
、LocalDateTime
或ZonedDateTime
來代替Date。由於Instant和其餘java.time包下的類是不可變的。Date已過期,不該再在新代碼中使用。
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());
}
複製代碼
這個有點像數據庫中的視圖
了,能夠給你看,但你不能修改源!
最後總結一下,防護性編程究竟是什麼呢?
防護性編程是一種比較泛化的概念,是一種細緻、謹慎的編程習慣。
咱們在寫代碼的時候,須要時刻考慮到:代碼是否正確?
代碼是否正確?
代碼是否正確?
例如:
緣由上面已經說明了!
Arrays.asList()返回一個ArrayList內部類,沒有add()
、remove()
、沒法改變長度
等,這樣設計的初衷是什麼?爲何不直接返回可變長的ArrayList(new ArrayList())?
和咱們剛剛的重寫getter方法相似,用於保證對象安全不可改變特性!
舉個例子,就是你有一個數組,怎麼設計一個方法:保證既能夠遍歷,又不能修改呢?
返回一個繼承了List接口
的輕量級「視圖」
不失爲一個好的設計方式。而直接返回數組則是不安全的選擇。
爲何須要不可變集合?
不可變對象有不少優勢,包括:
若是你沒有修改某個集合的需求,或者但願某個集合保持不變時,把它防護性地拷貝到不可變集合是個很好的實踐。
JDK的Collections類提供如下不可變集合,用於開發者的一些不可變需求:
同時,Guava亦提供如下不可變集合: