【面試突擊】Java中String類爲什麼要設計成final?

talk is cheap, show me the code.

什麼是不可變?

String a = "abcd";
a = "abcdef";

大多數人看了上面這兩句代碼,都認爲aabcd變成了abcdef,而且aString類型的,這句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類裏的valuefinal修飾,只是value在棧中存的這個數組的引用地址不可變,但是可以改變堆中的這個數組的內容。

看下面這個例子

final int[] value={1,2,3}
int[] another={4,5,6};
value = another;    //編譯器報錯,final不可變

valuefinal修飾,編譯器不允許我把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

再看下面這個HashSetStringBuilder做元素的場景,問題就更嚴重了,而且更隱蔽。

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型變量sb1sb2分別指向了堆內的字面量aaaaaabbb,並把它們插入到HashSet。
後面將sb1賦值給sb3,再改變sb3的值,因爲StringBuilder沒有不可變性的保護,
sb3直接在原先aaa的地址上改,導致sb1的值也變了。
這時候,HashSet上就出現了兩個內容相等的字符串aaabbb。破壞了HashSet元素的唯一性。
所以千萬不要用可變類型做HashMapHashSet

不可變性支持線程安全

在併發場景下,多個線程同時對一個資源進行寫操作,會出現線程安全的問題。
不可變對象不能被寫,所以是線程安全的。

總結

Q:Java中String類爲什麼要設計成final?
A:.安全性、效率

  • 安全性:final類型的類不能被繼承,並且String類中的final方法可以防止其內部的方法被重寫,亂改。
  • 效率:final類型的類被JVM當作內聯函數,提高了性能。

參考

在java中String類爲什麼要設計成final?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章