1. 程式人生 > >在java中String類為什麽要設計成final?

在java中String類為什麽要設計成final?

tro cli lai 這一 引用 沒有 num 重新 static

大神鏈接:在java中String類為什麽要設計成final? - 程序員 - 知乎

我進行了重新排版,並且更換了其中的一個例子,讓我們更好理解。

String很多實用的特性,比如說“不可變性”,是工程師精心設計的藝術品!藝術品易碎!用final就是拒絕繼承,防止世界被熊孩子破壞,維護世界和平!

1. 什麽是不可變?

String不可變很簡單,如下圖,給一個已有字符串"abcd"第二次賦值成"abcedl",不是在原內存地址上修改數據,而是重新指向一個新對象,新地址。

技術分享

2. String為什麽不可變?

翻開JDK源碼,java.lang.String類起手前三行,是這樣寫的:

[java]
view plain copy
  1. public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
  2. /** String本質是個char數組. 而且用final關鍵字修飾.*/
  3. private final char value[];
  4. ...
  5. ...
  6. }

首先String類是用final關鍵字修飾,這說明String不可繼承。再看下面,String類的主力成員字段value是個char[ ]數組,而且是用final

修飾的。final修飾的字段創建以後就不可改變。

有的人以為故事就這樣完了,其實沒有。因為雖然value是不可變,也只是value這個引用地址不可變。擋不住Array數組是可變的事實。Array的數據結構看下圖

技術分享

也就是說Array變量只是stack上的一個引用,數組的本體結構在heap堆。String類裏的value用final修飾,只是說stack裏的這個叫value的引用地址不可變。沒有說堆裏array本身數據不可變。看下面這個例子,

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

value用final修飾,編譯器不允許我把value指向堆區另一個地址。但如果我直接對數組元素動手,分分鐘搞定。

[java] view plain copy
  1. final int[] value={1,2,3};
  2. value[2]=100; //這時候數組裏已經是{1,2,100}

或者更粗暴的反射直接改,也是可以的。

[java] view plain copy
  1. final int[] array={1,2,3};
  2. 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
  1. package _12_01字符串;
  2. public class 為什麽String要設計成不可變類你 {
  3. public static void main(String[] args) {
  4. String a, b, c;
  5. a = "test";
  6. b = a;
  7. c = b;
  8. String processA = processA(a);
  9. String processB = processB(b);
  10. String processC = processC(c);
  11. System.out.println(processA);
  12. System.out.println(processB);
  13. System.out.println(processC);
  14. }
  15. static String processA(String str){
  16. return str + "A";
  17. }
  18. static String processB(String str){
  19. return str + "B";
  20. }
  21. static String processC(String str){
  22. return str + "C";
  23. }
  24. }
  25. //OUTPUT
  26. // testA
  27. //testB
  28. //testC

當String支持非可變性的時候,它們的值很好確定,不管調用哪個方法,都互不影響。

如果String是可變的,就可能如下例,我們使用StringBuffer來模擬String是可變的

[java] view plain copy
  1. package _12_01字符串;
  2. public class 為什麽String要設計成不可變類2 {
  3. public static void main(String[] args) {
  4. StringBuffer a, b, c;
  5. a = new StringBuffer("test");
  6. b = a;
  7. c = b;
  8. String processA = processA(a);
  9. String processB = processB(b);
  10. String processC = processC(c);
  11. System.out.println(processA);
  12. System.out.println(processB);
  13. System.out.println(processC);
  14. }
  15. static String processA(StringBuffer str){
  16. return str.append("A").toString();
  17. }
  18. static String processB(StringBuffer str){
  19. return str.append("B").toString();
  20. }
  21. static String processC(StringBuffer str){
  22. return str.append("C").toString();
  23. }
  24. }
  25. //OUTPUT
  26. // testA
  27. //testAB
  28. //testABC

能看出b=a,c=b;程序員的本意是希望變量是不變的。所以String不可變的安全性就體現在這裏。實際上StringBuffer的作用就是起到了String的可變配套類角色。

示例2

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

[java] view plain copy
  1. class Test{
  2. public static void main(String[] args){
  3. HashSet<StringBuilder> hs=new HashSet<StringBuilder>();
  4. StringBuilder sb1=new StringBuilder("aaa");
  5. StringBuilder sb2=new StringBuilder("aaabbb");
  6. hs.add(sb1);
  7. hs.add(sb2); //這時候HashSet裏是{"aaa","aaabbb"}
  8. StringBuilder sb3=sb1;
  9. sb3.append("bbb"); //這時候HashSet裏是{"aaabbb","aaabbb"}
  10. System.out.println(hs);
  11. }
  12. }
  13. //Output:
  14. //[aaabbb, aaabbb]

StringBuilder型變量sb1和sb2分別指向了堆內的字面量"aaa"和"aaabbb"。把他們都插入一個HashSet。到這一步沒問題。但如果後面我把變量sb3也指向sb1的地址,再改變sb3的值,因為StringBuilder沒有不可變性的保護,sb3直接在原先"aaa"的地址上改。導致sb1的值也變了。這時候,HashSet上就出現了兩個相等的鍵值"aaabbb"。破壞了HashSet鍵值的唯一性。所以千萬不要用可變類型做HashMap和HashSet鍵值。

不可變性支持線程安全

還有一個大家都知道,就是在並發場景下,多個線程同時讀一個資源,是不會引發竟態條件的。只有對資源做寫操作才有危險。不可變對象不能被寫,所以線程安全。

不可變性支持字符串常量池

最後別忘了String另外一個字符串常量池的屬性。像下面這樣字符串onetwo都用字面量"something"賦值。它們其實都指向同一個內存地址。

[java] view plain copy
  1. String one = "someString";
  2. String two = "someString";


技術分享

這樣在大量使用字符串的情況下,可以節省內存空間,提高效率。但之所以能實現這個特性,String的不可變性是最基本的一個必要條件。要是內存裏字符串內容能改來改去,這麽做就完全沒有意義了。

轉自:http://blog.csdn.net/u013905744/article/details/52414111

在java中String類為什麽要設計成final?