Java 中的 String 為什麼是不可變的?
什麼是不可變物件?
眾所周知, 在Java中, 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”還在記憶體中存在,並沒有改變。記憶體結構如下圖所示:
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中,陣列也是物件(可以參考我之前的文章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方法的原始碼可以說明問題:
讀者可以自己檢視其他方法,都是在方法內部重新建立新的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物件不可變。
相關文章
- [Java基礎]String 為什麼是不可變的?Java
- 你有沒有想過: 為什麼Java中String是不可變的?Java
- String,StringBuffer, StringBuilder 的區別是什麼?String為什麼是不可變的?UI
- 為什麼Java字串是不可變物件?Java字串物件
- Java中String類不可變性的好處Java
- 為什麼區塊鏈是不可篡改的區塊鏈
- 為什麼Java中繼承是有害的Java中繼繼承
- Java中建立不可變的類Java
- 為什麼Java中繼承多數是有害的Java中繼繼承
- 【Java系列】從JVM角度解析Java核心類String的不可變特性JavaJVM
- Java是什麼_Java是做什麼的?Java
- java 中/**是什麼Java
- Java中的不可變資料結構Java資料結構
- 深入理解Java中的不可變物件Java物件
- Java中如何快捷的建立不可變集合Java
- 為什麼Java String雜湊乘數為31?Java
- [Java] 變數裡存的到底是什麼Java變數
- Java中equals和==比的是什麼Java
- Java中Switch支援String字串?為什麼不支援long型別?Java字串型別
- 求求你,別問了,Java字串是不可變的Java字串
- 超硬核的Java工程師分享,什麼是Java?為什麼我要做Java,我是如何學習Java的?Java工程師
- java中RMI是什麼Java
- java中@Inherited是什麼?Java
- java中@Retention是什麼?Java
- Java中實現不可變MapJava
- java 中構建不可變物件Java物件
- 為什麼我認為《變數》是最好的塔防之一變數
- 什麼是java?為什麼大家都學習java技術?Java
- java中lock介面是什麼Java
- java中死鎖是什麼Java
- 【Java面試】Mybatis中#{}和${}的區別是什麼?Java面試MyBatis
- 藍碳是什麼,為什麼它是應對氣候變化的關鍵?
- 【Java面試】什麼是 ISR,為什麼需要引入 ISRJava面試
- 為什麼正好一次(Exactly-Once)傳遞是不可能的?
- Java是什麼?主要是幹什麼的?Java
- java中的StringJava
- Java 10中Stream API不可變集合JavaAPI
- Java中的不可變集合,我們換個方式理解!!!Java