String類設計成final的原因
我進行了重新排版,並且更換了其中的一個例子,讓我們更好理解。
String很多實用的特性,比如說“不可變性”,是工程師精心設計的藝術品!藝術品易碎!用final就是拒絕繼承,防止世界被熊孩子破壞,維護世界和平!
1. 什麼是不可變?
String不可變很簡單,如下圖,給一個已有字串“abcd”第二次賦值成“abcedl”,不是在原記憶體地址上修改資料,而是重新指向一個新物件,新地址。
2. String為什麼不可變?
翻開JDK原始碼,java.lang.String類起手前三行,是這樣寫的:
[java] view plain copy print?- publicfinalclass String
- /** String本質是個char陣列. 而且用final關鍵字修飾.*/
- privatefinalchar value[];
- …
- …
- }
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的資料結構看下圖
也就是說Array變數只是stack上的一個引用,陣列的本體結構在heap堆。String類裡的value用final修飾,只是說stack裡的這個叫value的引用地址不可變。沒有說堆裡array本身資料不可變。看下面這個例子,
- finalint[] value={1,2,3}
- int[] another={4,5,6};
- value=another; //編譯器報錯,final不可變
final int[] value={1,2,3}
int[] another={4,5,6};
value=another; //編譯器報錯,final不可變
value用final修飾,編譯器不允許我把value指向堆區另一個地址。但如果我直接對陣列元素動手,分分鐘搞定。
[java] view plain copy print?- finalint[] value={1,2,3};
- value[2]=100; //這時候數組裡已經是{1,2,100}
final int[] value={1,2,3};
value[2]=100; //這時候數組裡已經是{1,2,100}
或者更粗暴的反射直接改,也是可以的。
[java] view plain copy print?- finalint[] array={1,2,3};
- Array.set(array,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的私有訪問許可權的作用都比final大。而且設計師還很小心地把整個String設成final禁止繼承,避免被其他人繼承後破壞。所以String是不可變的關鍵都在底層的實現,而不是一個final。考驗的是工程師構造資料型別,封裝資料的功力。
3. 不可變有什麼好處?
這個最簡單的原因,就是為了安全。
示例1
[java] view plain copy print?- package _12_01字串;
- publicclass 為什麼String要設計成不可變類你 {
- publicstaticvoid main(String[] args) {
- String a, b, c;
- a = ”test”;
- b = a;
- c = b;
- String processA = processA(a);
- String processB = processB(b);
- String processC = processC(c);
- System.out.println(processA);
- System.out.println(processB);
- System.out.println(processC);
- }
- static String processA(String str){
- return str + “A”;
- }
- static String processB(String str){
- return str + “B”;
- }
- static String processC(String str){
- return str + “C”;
- }
- }
- //OUTPUT
- // testA
- //testB
- //testC
package _12_01字串;
public class 為什麼String要設計成不可變類你 {
public static void main(String[] args) {
String a, b, c;
a = "test";
b = a;
c = b;
String processA = processA(a);
String processB = processB(b);
String processC = processC(c);
System.out.println(processA);
System.out.println(processB);
System.out.println(processC);
}
static String processA(String str){
return str + "A";
}
static String processB(String str){
return str + "B";
}
static String processC(String str){
return str + "C";
}
}
//OUTPUT
// testA
//testB
//testC
當String支援非可變性的時候,它們的值很好確定,不管呼叫哪個方法,都互不影響。
如果String是可變的,就可能如下例,我們使用StringBuffer來模擬String是可變的
[java] view plain copy print?- package _12_01字串;
- publicclass 為什麼String要設計成不可變類2 {
- publicstaticvoid main(String[] args) {
- StringBuffer a, b, c;
- a = new StringBuffer(“test”);
- b = a;
- c = b;
- String processA = processA(a);
- String processB = processB(b);
- String processC = processC(c);
- System.out.println(processA);
- System.out.println(processB);
- System.out.println(processC);
- }
- static String processA(StringBuffer str){
- return str.append(“A”).toString();
- }
- static String processB(StringBuffer str){
- return str.append(“B”).toString();
- }
- static String processC(StringBuffer str){
- return str.append(“C”).toString();
- }
- }
- //OUTPUT
- // testA
- //testAB
- //testABC
package _12_01字串;
public class 為什麼String要設計成不可變類2 {
public static void main(String[] args) {
StringBuffer a, b, c;
a = new StringBuffer("test");
b = a;
c = b;
String processA = processA(a);
String processB = processB(b);
String processC = processC(c);
System.out.println(processA);
System.out.println(processB);
System.out.println(processC);
}
static String processA(StringBuffer str){
return str.append("A").toString();
}
static String processB(StringBuffer str){
return str.append("B").toString();
}
static String processC(StringBuffer str){
return str.append("C").toString();
}
}
//OUTPUT
// testA
//testAB
//testABC
能看出b=a,c=b;程式設計師的本意是希望變數是不變的。所以String不可變的安全性就體現在這裡。實際上StringBuffer的作用就是起到了String的可變配套類角色。
示例2
再看下面這個HashSet用StringBuilder做元素的場景,問題就更嚴重了,而且更隱蔽。
[java] view plain copy print?- class Test{
- publicstaticvoid main(String[] args){
- HashSet<StringBuilder> hs=new HashSet<StringBuilder>();
- 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);
- }
- }
- //Output:
- //[aaabbb, aaabbb]
class Test{
public static void main(String[] args){
HashSet<StringBuilder> hs=new HashSet<StringBuilder>();
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);
}
}
//Output:
//[aaabbb, aaabbb]
StringBuilder型變數sb1和sb2分別指向了堆內的字面量“aaa”和“aaabbb”。把他們都插入一個HashSet。到這一步沒問題。但如果後面我把變數sb3也指向sb1的地址,再改變sb3的值,因為StringBuilder沒有不可變性的保護,sb3直接在原先“aaa”的地址上改。導致sb1的值也變了。這時候,HashSet上就出現了兩個相等的鍵值“aaabbb”。破壞了HashSet鍵值的唯一性。所以千萬不要用可變型別做HashMap和HashSet鍵值。
不可變性支援執行緒安全
還有一個大家都知道,就是在併發場景下,多個執行緒同時讀一個資源,是不會引發竟態條件的。只有對資源做寫操作才有危險。不可變物件不能被寫,所以執行緒安全。
不可變性支援字串常量池
最後別忘了String另外一個字串常量池的屬性。像下面這樣字串one和two都用字面量“something”賦值。它們其實都指向同一個記憶體地址。[java] view plain copy print?
- String one = “someString”;
- String two = ”someString”;
String one = "someString";
String two = "someString";
這樣在大量使用字串的情況下,可以節省記憶體空間,提高效率。但之所以能實現這個特性,String的不可變性是最基本的一個必要條件。要是記憶體裡字串內容能改來改去,這麼做就完全沒有意義了。