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用來返回常量池中的某字符串,如果常量池中已經存在該字符串,則直接返回常量池中該對象的引用。否則,在常量池中加入該對象,然后 返回引用。
對於什么時候會在常量池存儲字符串對象:
- 顯示調用String的intern方法的時候,例如上例。
- 直接聲明字符串字面常量的時候,例如:
String a = "aaa";
- 直接
new String("A")
方法的參數使用常量的時候 - 字符串直接常量相加的時候,例如:
String c = "aa" + "bb";
其中的aa/bb只要有任何一個不是字符串字面常量形式,都不會在常量池生成"aabb". 且此時jvm做了優化,不會同時生成"aa"和"bb"在字符串常量池中
順便說一句,Integer、Long、Double……這幾個包裝類也是final的~