talk is cheap, show me the code.
什麼是不可變?
String a = "abcd";
a = "abcdef";
大多數人看了上面這兩句代碼,都認爲a
由abcd
變成了abcdef
,而且a
是String
類型的,這句String不可變
不攻自破啊?那麼真的是這樣嗎?
這個理解是錯誤的。大多數人對String不可變
這句話的理解都容易陷入上面這種思想。
而這兩句代碼的真正含義是:
首先將String
類型的變量a
賦值爲abcd
,再將變量a
賦值爲abcdef
。
進行第二次賦值時不是在原內存地址上進行修改數據,而是在堆中建了一個新的String
對象,並將棧中的引用指向了這個新對象,新地址。
所以abcd
這個字符串對象從創建出來後,始終都沒有被改變。
String爲什麼不可變?
翻開JDK源碼,java.lang.String類起手前三行,是這樣寫的:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** String本質是個char數組. 而且用final關鍵字修飾.*/
private final char value[];
...
...
}
首先String
類是用final
關鍵字修飾,這說明String
不可繼承。
再看下面,String
類的主力成員字段value
是個char[ ]
數組,而且是用final
修飾的。final
修飾的字段創建以後就不可改變。
有的人以爲故事就這樣完了,其實沒有。
因爲雖然value
是不可變,也只是value
這個引用地址不可變。
擋不住Array數組是可變的事實。
Array的數據結構看下圖
也就是說value
只是在棧中存了這個數組的引用地址,數組的本體結構在堆。
同理,String
類裏的value
用final
修飾,只是value
在棧中存的這個數組的引用地址不可變,但是可以改變堆中的這個數組的內容。
看下面這個例子
final int[] value={1,2,3}
int[] another={4,5,6};
value = another; //編譯器報錯,final不可變
value
用final
修飾,編譯器不允許我把value
指向堆區另一個地址。
但如果我直接對數組元素動手,分分鐘搞定。如:
final int[] value={1,2,3};
value[2]=100; //這時候數組裏已經是{1,2,100}
或者更粗暴的反射直接改,也是可以的。如:
final int[] array={1,2,3};
Array.set(array,2,100); //數組也被改成{1,2,100}
所以String
是不可變,關鍵是因爲SUN
公司的工程師,在後面所有String
的方法裏很小心的沒有去動Array
裏的元素,沒有暴露內部成員字段。
private final char value[]
這一句裏,使用private
修飾value
,保證外部不可見;使用final
修飾value
,保證內部不改變value
的引用。
而且設計師還很小心地把整個String
設成final
禁止繼承,避免被其他人繼承後破壞。
所以String
是不可變的關鍵都在底層的實現,而不單單是一個final
的功勞。
考驗的是工程師構造數據類型,封裝數據的功力。
不可變有什麼好處?
最簡單的原因,就是爲了安全。
示例1
String a, b, c;
a = "test";
b = a;
c = b;
a += "A";
System.out.println(a);
System.out.println(b);
System.out.println(c);
System.out.println();
b += "B";
System.out.println(a);
System.out.println(b);
System.out.println(c);
System.out.println();
c += "C";
System.out.println(a);
System.out.println(b);
System.out.println(c);
System.out.println();
控制檯輸出
testA
test
test
testA
testB
test
testA
testB
testC
從示例一
的輸出可以看出String爲不可變
的時候,b、c
是通過引用傳遞的方式進行賦值,
雖然一開始三個變量都指向了同一個地址,但是改變了a
的值,並沒有影響後面b、c
的使用。
如果String是可變的,就可能如下例,我們使用StringBuffer
來模擬String
是可變的:
StringBuffera, b, c;
a = new StringBuffer("test");
b = a;
c = b;
a.append("A");
System.out.println(a.toString());
System.out.println(b.toString());
System.out.println(c.toString());
System.out.println();
b.append("B");
System.out.println(a.toString());
System.out.println(b.toString());
System.out.println(c.toString());
System.out.println();
c.append("C");
System.out.println(a.toString());
System.out.println(b.toString());
System.out.println(c.toString());
System.out.println();
控制檯輸出
testA
testA
testA
testAB
testAB
testAB
testABC
testABC
testABC
我麼的本意是希望a、b、c
是不變的,是相互獨立的,結果卻並不是我們期望的那樣。
所以String
不可變的安全性就體現在這裏。
實際上StringBuffer
的作用就是起到了String
的可變配套類角色。
示例2
再看下面這個HashSet
用StringBuilder
做元素的場景,問題就更嚴重了,而且更隱蔽。
HashSet<StringBuilder> hs=new HashSet<>();
StringBuilder sb1 = new StringBuilder("aaa");
StringBuilder sb2 = new StringBuilder("aaabbb");
hs.add(sb1);
hs.add(sb2); // 這時候HashSet裏是{"aaa","aaabbb"}
StringBuilder sb3 = sb1;
sb3.append("bbb"); // 這時候HashSet裏是{"aaabbb","aaabbb"}
System.out.println(hs);
控制檯輸出
[aaabbb, aaabbb]
StringBuilder
型變量sb1
和sb2
分別指向了堆內的字面量aaa
和aaabbb
,並把它們插入到HashSet。
後面將sb1
賦值給sb3
,再改變sb3
的值,因爲StringBuilder
沒有不可變性的保護,
sb3
直接在原先aaa
的地址上改,導致sb1
的值也變了。
這時候,HashSet
上就出現了兩個內容相等的字符串aaabbb
。破壞了HashSet
元素的唯一性。
所以千萬不要用可變類型做HashMap
和HashSet
鍵
不可變性支持線程安全
在併發場景下,多個線程同時對一個資源進行寫操作,會出現線程安全的問題。
而不可變對象
不能被寫,所以是線程安全的。
總結
Q:Java中String類爲什麼要設計成final?
A:.安全性、效率
- 安全性:
final
類型的類不能被繼承,並且String
類中的final
方法可以防止其內部的方法被重寫,亂改。 - 效率:
final
類型的類被JVM
當作內聯函數,提高了性能。