對String不可變的理解
一直有寫部落格的打算,由於種種原因沒有開始,今天在公司正好討論到了String這個特殊的類,準備開啟自己的部落格之旅。
作為Java語言中應用最廣泛的String類,也可以算得上是最特殊的一個類,我們非常有必要深入瞭解它。
在《Thinking in java》第四版中有提到,“String類中每一個看起來會修改String值的方法,實際上都是建立了一個全新的String物件,以包含修改後的字串內容。而最初的String物件則絲毫未動。”
我們來看一下最常見的String方法replace(char oldChar, char newChar)。
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;
}
在原始碼中我們可以很明顯的看到replace方法呼叫之後返回的是一個新的String物件,也就是原來的String物件根本沒有改變。
我們可以做一個小測試。
public static void main(String[] args) {
String a = "abc";
StringBuilder sb = new StringBuilder("ccc");
System.out.println(new Test1().test(a));
System.out.println(a);
System.out.println(new Test1().test2(sb));
System.out.println(sb);
}
//不可變的String
public String test(String a) {
a += "bb";
return a;
}
//可變的StringBuilder
public StringBuilder test2(StringBuilder sb) {
return sb.append("xx");
}/* Output
abcbb
abc
cccxx
cccxx
*/
通過對比我們可以發現原來的String物件並沒有發生改變,返回的是一個新的String物件,而可變的StringBuilder在進行方法呼叫之後,原來的物件已經發生了改變。
翻開JDK原始碼。
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
...
}
首先Strng類用final修飾,說明無法繼承String。再看下面String類的主成員欄位是一個value字元陣列,用final修飾,不可改變。不過雖然不能改變,也只是無法改變value這個引用地址。指向的內容依然是可以改變的。除此之外還有一個hash變數,是該String物件的雜湊值快取。看一下例子,
public static void main(String[] args) {
final char value[] = { 'a', 'b', 'c' };
char another[] = { 'e', 'f', 'g' };
value = another;
} /*The final local variable value cannot be assigned. It must be blank and not using a compound assignment*/
編譯器間報錯,編譯器不允許我把value的引用指向heap記憶體中另外的地址。不過只要改變陣列元素,就可以搞定。
public static void main(String[] args) {
final char value[] = { 'a', 'b', 'c' };
value[0] = 'b';
System.out.println(value);
}/* Output:
bbc
*/
value字元陣列內容已經被改變了。
所以String不可變,其實是因為String方法沒有動value陣列的元素,沒有暴露內部成員欄位。String被final修飾,也導致整個String無法被繼承,不被破壞。
其實研究到這裡,腦海中已經有了一個大膽的想法,雖然value陣列引用沒有暴露,通過一般途徑無法獲取到,不過我們大可以用反射來訪問私有成員。
public static void testReflection() throws Exception {
//建立字串"Hello World", 並賦給引用s
String s = "Hello World";
System.out.println("s = " + s); //Hello World
//獲取String類中的value欄位
Field valueFieldOfString = String.class.getDeclaredField("value");
//改變value屬性的訪問許可權
valueFieldOfString.setAccessible(true);
//獲取s物件上的value屬性的值
char[] value = (char[])valueFieldOfString.get(s);
//改變value所引用的陣列中的第5個字元
value[5] = '_';
System.out.println("s = " + s); //Hello_World
}
在這個過程中s引用始終指向同一個物件,在反射前後,這個物件被改變了,也就是通過反射可以修改所謂的“不可變”物件。