1. 程式人生 > >對String不可變的理解

對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引用始終指向同一個物件,在反射前後,這個物件被改變了,也就是通過反射可以修改所謂的“不可變”物件。