Java String 真的是不可變的嗎

前言

這兩天在看Java面試相關的一些問題,很偶然也很幸運的看到了下面這篇文章。
http://blog.csdn.net/zhangjg_blog/article/details/18319521

這篇文章的作者有一系列關于Java深入學習的文章,很值得一看,個人覺得非常好,很有收獲。

看完那篇文章有一段評論讓我很有感觸。忍不住寫了點代碼做了下測試,加以驗證。

起因

問答

正如我們所理解的,通過

String hello = "Hello World!";

String xx = new String("Hello World!");

得到的字符串對象是不一樣的,new方式是在堆空間中創建的,而直接的字符串則是先被放到常量池中。如果有新的與之一樣的對象被創建,則直接讓這個新對象引用常量池中的這個地址即可。

這樣的好處就是可以最大限度的節省內存空間。

而使用new方式創建的則就不一樣了,只要是用了new創建字符串,就會在堆空間中開辟出一塊內存,然后返回這個內存地址的引用。所以這樣創建的對象,即使內容一致,也不會是指向同一個內存地址。

下面用幾個簡單的代碼做下測試。

/**
*字符串中對于內容和地址的判定可以用下面兩種方式,但側重點不一樣。
*/
equals  // 判斷 兩個字符串的內容是否一致
==      // 判斷兩個字符串的內存地址是否一致

且看下面的代碼:

public static void simple() {
        String s1 = "Hello World!";
        String s2 = "Hello World!";
        String s3 = new String("Hello World!");
        String s4 = new String("Hello World!");
        
        // 下面開始比較引用和內容的比較
        System.out.println("字符串賦值方式:");
        System.out.println(s1==s2);
        System.out.println(s1.equals(s2));
        
        System.out.println("\n字符串賦值方式和new方式:");
        System.out.println(s1==s3);
        System.out.println(s1.equals(s3));
        
        System.out.println("\nnew 方式:");
        System.out.println(s3==s4);
        System.out.println(s3.equals(s4));
    }

得到的結果如下:

字符串賦值方式:
true
true

字符串賦值方式和new方式:
false
true

new 方式:
false
true

結果卻是和我們所說的那樣。

深入源碼

不出所料,String確實是“不可變的”,每次改變底層其實都是創建了一個心的字符串對象,然后賦予了新值。
為什么會這樣呢?我們也許可以在源碼中找到真相。


字符串源碼

哦,原來Java對于String類只是維護了一個final類型的字符數組啊。怪不得賦值之后就不能改變了呢。

但是也許你會有疑問,咦,不對啊,“我經常使用String的什么replace方法改變字符串的內容啊。你這則么解釋呢?”

其實答案還是那樣,它真的沒變,我們并沒有看到事情的真相,相信看完下面的源碼,你就明白了。

/**
     * Returns a string resulting from replacing all occurrences of
     * {@code oldChar} in this string with {@code newChar}.
     * <p>
     * If the character {@code oldChar} does not occur in the
     * character sequence represented by this {@code String} object,
     * then a reference to this {@code String} object is returned.
     * Otherwise, a {@code String} object is returned that
     * represents a character sequence identical to the character sequence
     * represented by this {@code String} object, except that every
     * occurrence of {@code oldChar} is replaced by an occurrence
     * of {@code newChar}.
     * <p>
     * Examples:
     * <blockquote><pre>
     * "mesquite in your cellar".replace('e', 'o')
     *         returns "mosquito in your collar"
     * "the war of baronets".replace('r', 'y')
     *         returns "the way of bayonets"
     * "sparring with a purple porpoise".replace('p', 't')
     *         returns "starring with a turtle tortoise"
     * "JonL".replace('q', 'x') returns "JonL" (no change)
     * </pre></blockquote>
     *
     * @param   oldChar   the old character.
     * @param   newChar   the new character.
     * @return  a string derived from this string by replacing every
     *          occurrence of {@code oldChar} with {@code 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;
    }

源碼中很明確的使用了

new String(buf, true);

的方式返回給調用者新對象了。

真的不可變嗎?

讀到上面的內容,其實基本上已經夠了。但是了解一下更深層次的內容,相信對我們以后編程來說會更好。
源碼中清楚的使用char[] value來盛裝外界的字符串數據。也就是說字符串對象的不可變的特性,其實是源自value數組的final特性。

那么我們可以這么想,我們不改變String的內容,而是轉過頭來改變value數組的內容(可以通過反射的方式來修改String對象中的private屬性的value),結果會怎樣呢?

答案是真的會變哦。

可以先看下下面的代碼

private static void deep() throws NoSuchFieldException, IllegalAccessException {
        String hello = "Hello World!";
        String xx = new String("Hello World!");
        String yy = "Hello World!";
        
        /**
         * 判斷字符串是否相等,默認以內存引用為標準
         */
        System.out.println(hello == xx);
        System.out.println(hello == yy);
        System.out.println(xx == yy);
        
        // 查看hello, xx, yy 三者所指向的value數組的真實位置
        Field hello_field = hello.getClass().getDeclaredField("value");
        hello_field.setAccessible(true);
        char[] hello_value = (char[]) hello_field.get(hello);
        System.out.println( hello_field.get(hello));
        
        Field xx_field = xx.getClass().getDeclaredField("value");
        xx_field.setAccessible(true);
        char[] xx_value = (char[]) xx_field.get(xx);
        System.out.println(xx_field.get(xx));
        
        Field yy_field = yy.getClass().getDeclaredField("value");
        yy_field.setAccessible(true);
        char[] yy_value = (char[]) yy_field.get(yy);
        System.out.println(yy_field.get(yy));
        /**
         * 經過反射獲取到這三個字符串對象的最底層的引用數組value,發現如果一開始內容一致的話,java底層會將創建的字符串對象指向同一個字符數組
         * 
         */
        
        // 通過反射修改字符串引用的value數組
        Field field = hello.getClass().getDeclaredField("value");
        field.setAccessible(true);
        char[] value = (char[]) field.get(hello);
        System.out.println(value);
        value[5] = '^';
        System.out.println(value);
        
        // 驗證xx是否被改變
        System.out.println(xx);
    }

結果呢?

false
true
false
[C@6d06d69c
[C@6d06d69c
[C@6d06d69c
Hello World!
Hello^World!
Hello^World!

真的改變了。

而我們也可以發現,hello,xx, yy最終都指向了內存中的同一個value字符數組。這也說明了Java在底層做了足夠強的優化處理。

當創建了一個字符串對象時,底層會對應一個盛裝了相應內容的字符數組;此時如果又來了一個同樣的字符串,對于value數組直接獲取剛才的那個引用即可。(相信我們都知道,在Java中數組其實也是一個對象類型的數據,這樣既不難理解了)。

不管是字符串直接引用方式,還是new一個新的字符串的方式,結果都是一樣的。它們內部的字符數組都會指向內存中同一個“對象”(value字符數組)。

總結

稍微有點亂,但是從這點我們也可以看出String的不可變性其實仍舊是對外界而言的。在最底層,Java把這一切都給透明化了。我們只需要知道String對象有這點特性,就夠了。

其他的,日常應用來說,還是按照String對象不可變來使用即可。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容