Java中String類為什么被設計為final?


Java中String類為什么被設計為final

  首先,String是引用類型,也就是每個字符串都是一個String實例。通過源碼可以看到String底層維護了一個byte數組:private final byte[] value;(JDK9中為byte數組,並非網上所說的char數組)。雖然該數組被修飾為final,但這並不能保證數組的數據不會變化,因此還需要聲明為private防止被其他類修改數據。
  被final修飾的類不能被繼承,也就是不能有子類。那么為什么要把String設計為不能被繼承呢?簡單來說有兩點:安全效率

安全

  要知道String是一個非常非常基礎的類,用處超級廣泛,各種各樣的類基本都使用到了字符串。
  假設String類可以被繼承,現在有一個方法method,該方法的參數為String類型,並且該方法利用到了字符串的長度特性:

public int method(String s){
    //do something
    int a = s.length() + 1;
    
    return a;
}

我們設計出一個String的子類MyString,並重寫了其長度方法:

public class MyString{
    @Override
    public int length(){
        return 0;
    }
}

  基於Java的多態特性,當我們把MyString的實例作為參數傳入method()方法時,編譯器是不會報錯的。但是我們的運行結果則完全錯誤,這會造成非常嚴重的后果。

MyString myString = new MyString();
method(myString);//此時編譯並不會報錯,但是運行結果是完全錯誤的。

  相對於每次使用字符串的時候使用final修飾,直接把String類定義為final更為安全,效率也更高。並且,整個類聲明為final之后,如果有一個String的引用,則它引用的一定是String對象,而不會是其他類的對象(泛型允許引用子類)。防止世界被熊孩子破壞2333

  除了由多態引起的安全問題,還有引用類型本身的問題。
  比如現在有兩個方法,appendStr負責在不可變的String參數后添加“bbb”並返回,appendSb負責在可變的StringBuilder后添加“bbb”並返回。

public static String appendStr(String s){
    s = s + "bbb";
    return s;
}

public static StringBuilder appendSb(StringBuilder sb){
    sb.append("bbb");
    return sb;
}

public static void main(String[] args) {
    //String做參數
    String str = new String("aaa");
    String newStr = appendStr(str);
    System.out.println("String aaa -> " + str.toString());

    //StringBuilder做參數
    StringBuilder sb = new StringBuilder("aaa");
    StringBuilder newSb = appendSb(sb);
    System.out.println("StringBuilder aaa -> " + newSb.toString());
}

但實際輸出結果卻是:

String aaa -> aaa
StringBuilder aaa -> aaabbb

  如果程序員不小心像上面例子里,直接在傳進來的參數上加"bbb",因為Java對象參數傳的是引用,所以可變的的StringBuffer參數就被改變了。可以看到變量sb在Test.appendSb(sb)操作之后,就變成了"aaabbb"。有的時候這可能不是程序員的本意。所以String不可變的安全性就體現在這里。
  再看下面這個HashSet用StringBuilder做元素的場景,問題就更嚴重了,而且更隱蔽。

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);//輸出:[aaabbb, aaabbb]
}

  這就破壞了HashSet鍵的唯一性,因此千萬不要使用可變類型做HashMap和HashSet的鍵值。(不可變的字符串則非常適合作為鍵)

  除了上述兩種問題,不可變的字符串還可以保證多線程時的線程安全問題。多線程時,只有讀操作一般不會引發線程安全問題,當讀寫同時存在時便容易引發安全問題。當字符串不可變時也就不能寫,當然不會引發線程問題。

效率

  基於字符串的不可變,才能有字符串常量池這一特性。字符串常量池的誕生是為了提升效率和減少內存分配。可以說我們編程有百分之八十的時間在處理字符串,而處理的字符串中有很大概率會出現重復的情況。正因為String的不可變性,常量池很容易被管理和優化。
  並且1.7之前,字符串常量池在方法區,1.7之后在堆內存中,並且不僅僅可以存儲對象,還可以存儲對象的引用:

String s = new String("A") + new String("B");//此時常量池存在"A"、"B",但是不存在"AB";堆中存在"A"、"B"、"AB",並且s指向"AB"
s.intern();//1.7之后這里加入的是對象s的引用,而非直接保存"AB"字符串
//intern用來返回常量池中的某字符串,如果常量池中已經存在該字符串,則直接返回常量池中該對象的引用。否則,在常量池中加入該對象,然后 返回引用。

  對於什么時候會在常量池存儲字符串對象:

  1. 顯示調用String的intern方法的時候,例如上例。
  2. 直接聲明字符串字面常量的時候,例如: String a = "aaa";
  3. 直接new String("A")方法的參數使用常量的時候
  4. 字符串直接常量相加的時候,例如: String c = "aa" + "bb"; 其中的aa/bb只要有任何一個不是字符串字面常量形式,都不會在常量池生成"aabb". 且此時jvm做了優化,不會同時生成"aa"和"bb"在字符串常量池中

順便說一句,Integer、Long、Double……這幾個包裝類也是final的~

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM