String源碼分析之Java中的String為什麼是不可變的以及replace方法源碼分析

來源:互聯網
上載者:User

標籤:string

什麼是不可變對象?

眾所周知, 在Java中, String類是不可變的。那麼到底什麼是不可變的對象呢? 可以這樣認為:如果一個對象,在它建立完成之後,不能再改變它的狀態,那麼這個對象就是不可變的。不能改變狀態的意思是,不能改變對象內的成員變數,包括基礎資料型別 (Elementary Data Type)的值不能改變,參考型別的變數不能指向其他的對象,參考型別指向的對象的狀態也不能改變。

區分對象和對象的引用

對於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”還在記憶體中存在,並沒有改變。記憶體結構如所示:



Java和C++的一個不同點是, 在Java中不可能直接操作對象本身,所有的對象都由一個引用指向,必須通過這個引用才能訪問對象本身,包括擷取成員變數的值,改變對象的成員變數,調用對象的方法等。而在C++中存在引用,對象和指標三個東西,這三個東西都可以訪問對象。其實,Java中的引用和C++中的指標在概念上是相似的,他們都是存放的對象在記憶體中的地址值,只是在Java中,引用喪失了部分靈活性,比如Java中的引用不能像C++中的指標那樣進行加減運算。

為什麼String對象是不可變的?
要理解String的不可變性,首先看一下String類中都有哪些成員變數。 在JDK1.6中,String的成員變數有以下幾個:

public final class String    implements java.io.Serializable, Comparable<String>, CharSequence{    /** The value is used for character storage. */    private final char value[];    /** The offset is the first index of the storage that is used. */    private final int offset;    /** The count is the number of characters in the String. */    private final int count;    /** Cache the hash code for the string */    private int hash; // Default to 0

在JDK1.7中,String類做了一些改動,主要是改變了substring方法執行時的行為,這和本文的主題不相關。JDK1.7中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類其實就是對字元數組的封裝。JDK6中, value是String封裝的數組,offset是String在這個value數組中的起始位置,count是String所佔的字元的個數。在JDK7中,只有一個value變數,也就是value中的所有字元都是屬於String這個對象的。這個改變不影響本文的討論。 除此之外還有一個hash成員變數,是該String對象的雜湊值的緩衝,這個成員變數也和本文的討論無關。在Java中,數組也是對象。 所以value也只是一個引用,它指向一個真正的數組對象。其實執行了String s = “ABCabc”; 這句代碼之後,真正的記憶體布局應該是這樣的:



value,offset和count這三個變數都是private的,並且沒有提供setValue, setOffset和setCount等公用方法來修改這些值,所以在String類的外部無法修改String。也就是說一旦初始化就不能修改, 並且在String類的外部不能訪問這三個成員。此外,value,offset和count這三個變數都是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等方法都存在返回值的原因。也是為什麼像下面這樣調用不會改變對象的值:

列印結果:
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", 並賦給引用sString 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對象不可變。


附:

我們回過頭來看一下replace的源碼這段代碼的精髓之處:

1.為了程式更快,所以才先確保原來的字串中有舊的字元,不然會白白迴圈很多次。
2.找到後,建立一個新的字串buf,長度和原字串一樣,再用while迴圈替換所有的舊字元.
 
我們來分析一下這樣做的效率提升:
如果不判斷字串是否有舊字元,直接執行:

while (i < len){                    char c = val[i];                    buf[i] = (c == oldChar) ? newChar : c;                    i++;}

有n個字元,每個字元要執行判斷的次數為: while語句1次,char c賦值1次,c==oldChar判斷1次,賦值給buf[i] 1次
i++ 1次,所以總運算元為  5n。
 
而先判斷有舊字元再while迴圈,找到舊字元前每個字元只要執行3次操作,明顯提升了程式效率。

String源碼分析之Java中的String為什麼是不可變的以及replace方法源碼分析

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.