從String源碼看-爲什麼JDK中的String不可變

什麼是不可變對象?

       我們都知道, 在Java中, String類是不可變的。那麼到底什麼是不可變的對象呢? 可以這樣認爲:如果一個對象,在它創建完成之後,不能再改變它的狀態,那麼這個對象就是不可變的。不能改變狀態的意思是,不能改變對象內的成員變量,包括基本數據類型的值不能改變,引用類型的變量不能指向其他的對象,引用類型指向的對象的狀態也不能改變。

區分對象和對象的引用

String s = "ABCabc";  
System.out.println("s = " + s);  

s = "123456";  
System.out.println("s = " + s);  


打印結果爲:
s = ABCabc
s = 123456

       首先創建一個String對象s,然後讓s的值爲“ABCabc”, 然後又讓s的值爲“123456”。 從打印結果可以看出,s的值確實改變了。那麼怎麼還說String對象是不可變的呢? 其實這裏存在一個誤區: s只是一個String對象的引用,並不是對象本身。對象在內存中是一塊內存區,成員變量越多,這塊內存區佔的空間越大。引用只是一個4字節的數據,裏面存放了它所指向的對象的地址,通過這個地址可以訪問對象。
       也就是說,s只是一個引用,它指向了一個具體的對象,當s=“123456”; 這句代碼執行過之後,又創建了一個新的對象“123456”, 而引用s重新指向了這個心的對象,原來的對象“ABCabc”還在內存中存在,並沒有改變。內存結構如下圖所示:
這裏寫圖片描述

爲什麼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

       由以上的代碼可以看出, 在Java中String類其實就是對字符數組的封裝。value是String封裝的數組,value中的所有字符都是屬於String這個對象的。還有一個hash成員變量,是該String對象的哈希值的緩存。在Java中,數組也是對象。所以value也只是一個引用,它指向一個真正的數組對象。其實執行了String s = “ABCabc”; 這句代碼之後,真正的內存佈局應該是這樣的:
這裏寫圖片描述
       value是private的,並且沒有提供setValue公共方法來修改值,所以在String類的外部無法修改String。也就是說一旦初始化就不能修改, 並且在String類的外部不能訪問該變量。此外,value是final的, 也就是說在String類內部,一旦這個值初始化了, 也不能被改變。所以可以認爲String對象是不可變的了。

那麼在String中,明明存在一些方法,調用他們可以得到改變後的值。這些方法包括substring, replace, replaceAll, toLowerCase等。例如如下代碼:

String a = "ABCabc";  
System.out.println("a = " + a);  
a = a.replace('A', 'a');  
System.out.println("a = " + a);  

打印結果爲:
a = ABCabc
a = aBCabc

那麼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對象,並且返回這個新的對象,原來的對象是不會被改變的。這也是爲什麼像replace, substring,toLowerCase等方法都存在返回值的原因。也是爲什麼像下面這樣調用不會改變對象的值:

String ss = "123456";  

System.out.println("ss = " + ss);  

ss.replace('1', '0');  

System.out.println("ss = " + ss);  


打印結果:
ss = 123456
ss = 123456

那麼String對象真的不可變嗎?

     從上文可知String的成員變量是private final 的,也就是初始化之後不可改變。那麼在這幾個成員中, value比較特殊,因爲他是一個引用變量,而不是真正的對象。value是final修飾的,也就是說final不能再指向其他數組對象,那麼我能改變value指向的數組嗎? 比如將數組中的某個位置上的字符變爲下劃線“_”。 至少在我們自己寫的普通代碼中不能夠做到,因爲我們根本不能夠訪問到這個value引用,更不能通過這個引用去修改數組。
那麼用什麼方式可以訪問私有成員呢? 沒錯,用反射, 可以反射出String對象中的value屬性, 進而改變通過獲得的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 = Hello World
s = Hello_World

在這個過程中,s始終引用的同一個String對象,但是再反射前後,這個String對象發生了變化, 也就是說,通過反射是可以修改所謂的“不可變”對象的。但是一般我們不這麼做。這個反射的實例還可以說明一個問題:如果一個對象,他組合的其他對象的狀態是可以改變的,那麼這個對象很可能不是不可變對象。例如一個Car對象,它組合了一個Wheel對象,雖然這個Wheel對象聲明成了private final 的,但是這個Wheel對象內部的狀態可以改變, 那麼就不能很好的保證Car對象不可變。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章