為什么String類是不可變的?
#
String類
什么是不可變對象
當滿足以下條件時,對象才是不可變的:
- 對象創建以后其狀態就不能修改。
- 對象的所有域都是final類型的。
- 對象是正確創建的(在對象的創建期間,this引用沒有逸出)。
這是《Java並發編程實戰》一書中的定義。在書中,說明並不是一定要將所有的域都設為final類型,比如String類就是這種情況,String會將散列值的計算推遲到第一次調用hashCode()時進行,並將計算得到的散列值緩存到非final類型的域中,但這種方式之所以可行,是因為在每次計算時都得到相同的結果,這基於一個不可變的狀態(書中指出:自己編寫代碼時不要這么做,推薦全都設為final)。
因此,不可變對象可以理解為:如果一個對象,在它正確創建完成之后,不能再改變它的狀態(包括基本數據類型的值不能改變,引用類型的變量不能指向其他的對象,引用類型指向的對象的狀態也不能改變),那么這個對象就是不可變的。
“不可變的對象”與“不可變的對象引用”區別
比如:
String str = "test";
str = "test1";
我們從下圖可以看到,當定義String str = "test1"時,其實不是真正改變了str的內容,而是改變了str的引用。
那么何為"不可變的對象引用"呢?final只保證引用類型變量所引用的地址不會改變,即一直引用同一個對象,但是這個對象的內容(對象的非final成員變量的值可以改變)完全可以發生改變(比如final int[] intArray;,intArray不允許再引用其他對象,但是intArray內的int值卻可以被修改)。
為什么String對象是不可變的?
要理解String的不可變性,首先看一下String類中都有哪些成員變量。 在JDK1.8中,String的成員變量有以下幾個:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
其中,成員變量hash並沒有用final聲明,但是由於第一次調用hashCode()會重新計算hash值,並且以后調用會使用已緩存的值,當然最關鍵的是每次計算時都得到相同的結果,所以也保證了對象的不可變。
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
在Java中,數組也是對象, 所以value也只是一個引用,它指向一個真正的數組對象。其實執行了String s = “ABCabc”; 這句代碼之后,真正的內存布局應該是這樣的:
value是String封裝的數組,value中的所有字符都是屬於String這個對象的。由於value是private的,並且沒有提供setValue等公共方法來修改這些值,所以在String類的外部無法修改String。也就是說一旦初始化就不能修改。此外,value變量是final的, 也就是說在String類內部,一旦這個值初始化了,value引用類型變量所引用的地址不會改變,即一直引用同一個對象。所以可以說String對象是不可變對象。但其實value所引用對象的內容完全可以發生改變(反射消除String類對象的不可變特性)。
如何理解substring, replace, replaceAll, toLowerCase等方法
比如:
String a = "ABCabc";
System.out.println("a = " + a);
a = a.replace('A', 'a');
System.out.println("a = " + a);
a的值看似改變了,其實也是同樣的誤區。再次說明, a只是一個引用, 不是真正的字符串對象,在調用a.replace('A', 'a')時, 方法內部創建了一個新的String對象,並把這個新的對象重新賦給了引用a。String中replace方法的源碼可以說明問題:
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
String類不可變性的好處
- 只有當字符串是不可變的,字符串池才有可能實現。字符串池的實現可以在運行時節約很多heap空間,因為不同的字符串引用可以指向池中的同一個字符串。但如果字符串是可變的,如果變量改變了它的值,那么其它指向這個值的變量的值也會一起改變。
- 如果字符串是可變的,那么會引起很嚴重的安全問題。譬如,數據庫的用戶名、密碼都是以字符串的形式傳入數據庫,以獲得數據庫的連接,或者在socket編程中,主機名和端口都是以字符串的形式傳入。因為字符串是不可變的,所以它的值是不可改變的,否則黑客們可以鑽到空子,改變字符串指向的對象的值,造成安全漏洞。
- 因為字符串是不可變的,所以是多線程安全的,同一個字符串實例可以被多個線程共享。這樣便不用因為線程安全問題而使用同步。
- 類加載器要用到字符串,不可變性提供了安全性,以便正確的類被加載。譬如你想加載java.sql.Connection類,而這個值被改成了myhacked.Connection,那么會對你的數據庫造成不可知的破壞。
- 因為字符串是不可變的,所以在它創建的時候hashcode就被緩存了,不需要重新計算,這就使得字符串很適合作為Map中的鍵,字符串的處理速度要快過其它的鍵對象。這就是HashMap中的鍵往往都使用字符串的原因。