Java中的String爲何是不可變的? -- String源碼分析

什麼是不可變對象?

    衆所周知, 在Java中, String類是不可變的。那麼到底什麼是不可變的對象呢? 能夠這樣認爲:若是一個對象,在它建立完成以後,不能再改變它的狀態,那麼這個對象就是不可變的。不能改變狀態的意思是,不能改變對象內的成員變量,包括基本數據類型的值不能改變,引用類型的變量不能指向其餘的對象,引用類型指向的對象的狀態也不能改變。java

區分對象和對象的引用

對於Java初學者, 對於String是不可變對象老是存有疑惑。看下面代碼:sql

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」; 這句代碼以後,真正的內存佈局應該是這樣的:socket

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對象不可變。

 

String類不可變性的好處

  1. 只有當字符串是不可變的,字符串池纔有可能實現。字符串池的實現能夠在運行時節約不少heap空間,由於不一樣的字符串變量都指向池中的同一個字符串。但若是字符串是可變的,那麼String interning將不能實現(String interning是指對不一樣的字符串僅僅只保存一個,即不會保存多個相同的字符串),由於這樣的話,若是變量改變了它的值,那麼其它指向這個值的變量的值也會一塊兒改變。
  2. 若是字符串是可變的,那麼會引發很嚴重的安全問題。譬如,數據庫的用戶名、密碼都是以字符串的形式傳入來得到數據庫的鏈接,或者在socket編程中,主機名和端口都是以字符串的形式傳入。由於字符串是不可變的,因此它的值是不可改變的,不然黑客們能夠鑽到空子,改變字符串指向的對象的值,形成安全漏洞。
  3. 由於字符串是不可變的,因此是多線程安全的,同一個字符串實例能夠被多個線程共享。這樣便不用由於線程安全問題而使用同步。字符串本身即是線程安全的。
  4. 類加載器要用到字符串,不可變性提供了安全性,以便正確的類被加載。譬如你想加載java.sql.Connection類,而這個值被改爲了myhacked.Connection,那麼會對你的數據庫形成不可知的破壞。
  5. 由於字符串是不可變的,因此在它建立的時候hashcode就被緩存了,不須要從新計算。這就使得字符串很適合做爲Map中的鍵,字符串的處理速度要快過其它的鍵對象。這就是HashMap中的鍵每每都使用字符串。

做者:程序媛小雙
連接:https://www.zhihu.com/question/20618891/answer/147575525
來源:知乎
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

 

-----------------------------------------------------------------------------------------------------------

shilvfei寫的測試代碼:

package string.test;

import java.lang.reflect.Field;

/**
 * @description
 * 
 * @author shilvfei
 * 
 * @date 2018年5月7日
 */
public class StringTest {

	public static void main(String[] args) throws Exception {
		//test01();
		test02();
	}

	private static void test01() {
		String s = "abc";
		System.out.println("替換前s:"+s);
	
		String newS = s.replaceAll(s,"def");
		System.out.println("替換後s:"+s);
		System.out.println("newS:"+newS);
	}
	
	//String 真的是不可變的嗎?
	private static void test02() throws NoSuchFieldException, IllegalAccessException {
		String s = "hello World";
		System.out.println("s = "+s);
		
		//獲取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);
	}
	
	
}
相關文章
相關標籤/搜索