從Java的字元串池、常量池理解String的intern()

前言

逛知乎遇到一個剛學Java就會接觸的字元串比較問題:
在这里插入图片描述
通常,根據"==比較的是地址,equals比較的是值"介個定理就能得到結果。但是String有些特殊,通過new String(string)生成的兩個同值的字元串地址就不相等,用其他方式來生成的兩個同值字元串地址就相等。

程式碼如下:

	// 第一種方式創建字元串,字面量賦值
	String str1 = "abc";
	String str2 = "abc";
	// 第二種方式創建字元串
    String str3 = new String("xyz");
    String str4 = new String("xyz");
    System.out.println(str1 == str2); //true
    System.out.println(str3 == str4); //false

同樣是創建字元串,兩對等值的字元串進行為什麼結果不一樣,這就涉及到了常量池和堆。

第一種方式創建的字元串,會將"abc"這個字面量放到了常量池中,然後str1和str2都指向常量池中的"abc",所以兩個變數地址相同;第二種方式創建的字元串,是先在常量池中放入"xyz",然後通過構造函數將常量池中的"xyz"拷貝一份到堆中生成新的String,和常量池中的"xyx"就沒有了關係,所以兩個變數指向的是堆中兩個不同的變數,所以兩個變數地址不同。

那intern()又是啥?和常量池之間又有什麼聯繫?

常量池

常量池是存放字面量、符號引用或直接引用的地方。而常量池又分為class常量池和運行時常量池。

class常量池

class常量池是存放編譯期類中的字面量和符號引用。上面的字元串"abc"就是字面量;符號引用就是類和介面的完全限定名,欄位的名稱和描述符,方法的名稱和描述符。

如圖:
符号引用句柄
圖中的就是new String(String)這個方法在常量池中的名稱和描述符,即符號引用。

運行時常量池

我們平時說的常量池指的就是運行時常量池。在類載入的解析階段,會將class常量池載入記憶體中(JDK1.7之前位於方法區,現在位於Heap中),並且將符號引用解析成直接引用,即根據對方法/類的描述資訊指向記憶體中對應的方法/類。運行時常量池具有動態性,可以在運行期添加新的變數進入常量池。

intern()

先看一下intern()這個方法的描述:intern description
用二級英文水平翻譯一波,大意就是一個string調用intern()的時候,如果池中有和這個字元串值相等的字元串對象,就會將字元串池中的字元串對象返回;如果沒有,就將這個字元串添加進去,並返回這個字元串的引用。字元串池由String類私有維護。

這裡又引入了字元串池這個概念。

字元串池

字元串池存放的是常量池中字元串對象的引用,而不是字元串對象。通過第一種字面量賦值法創建的字元串會放在常量池中,字元串池就會存儲這個字元串對象的引用,當再次在常量池創建字元串時,會先從字元串池查看是否有此字元串的等值引用,如果有的話,直接指向此引用對應的對象。

而第二種方式創建的字元串,會在字元串池中查找是否有與構造參數等值的字元串,以此決定是否需要在常量池新建字元串,然後拷貝常量池中字元串在Heap創建一個新的字元串。
在这里插入图片描述
如圖,在堆中會在常量池中創建一個名為original的新字元串,然後拷貝並在堆中生成一個新字元串。注釋中也提到,除非你需要一個字元串的顯式副本,否則不需要使用這個構造函數,因為字元串是不可變的。

這裡使用intern()測試一下字元串池:

    public static void main(String[] args) {
    	  //第一部分 測試
        String str1 = "abc";
        String str2 = new String("abc");
        System.out.println(str1.intern() == str1); //true
        System.out.println(str1.intern() == str2); //false
        System.out.println(str1.intern() == str2.intern()); //true
        //第二部分 測試通過char[]創建字元串後,引用是否會進入字元串池
        String str3 = new String(new char[]{'g', 'h'});
        String str4 = "gh";
        System.out.println(str3.intern() == str3); //false
        System.out.println(str3.intern() == str4); //true
        //第三部分 測試char[]創建的字元串調用intern()後引用是否進入字元串池
        String str3 = new String(new char[]{'g', 'h'});
        str3.intern();
        String str4 = "gh";
        System.out.println(str3.intern() == str3); //true
        System.out.println(str3.intern() == str4); //true
    }

以上三部分程式碼是獨立測試。

第一部分:str1在常量池創建了abc,並將引用放入字元串池,str2拷貝常量池中的abc並在堆中創建新字元串。intern()從字元串池中獲取的是常量池中str1的abc引用。

第二部分:str3通過char[]在堆中創建了字元串,不是在常量池,所以gh的引用不會自動放入字元串池。str4在常量池創建了gh,所以字元串池中保存了str4的gh引用。intern()從字元串池中獲取的是常量池中str4的gh引用。

第三部分:str3通過char[]在堆中創建了字元串,不是在常量池,所以gh的引用不會自動放入字元串池,但是它調用intern()手動將str3的gh的引用添加到了字元串池中。當str4使用字面量賦值創建時,查詢到字元串池中有gh的引用,str4就指向了str3的gh引用。intern()從字元串池中獲取的是堆中str3的gh引用。

從上面的程式碼中也得出結論:​intern()可以將堆中創建的且字元串池沒有等值引用的字元串引用放入字元串池。

同時,這也能說明String為什麼不可變這個問題。
因為這樣可以保證多個引用可以同時指向字元串池中的同一個對象。如果字元串是可變的,其中的一個引用操作改變了對象的值,對其他引用會有影響,這樣顯然是不可以的。

言歸正傳

回到知乎上的問題。在常量池創建了"string"並將其引用放入字元串池,str1調用intern()返回的是常量池中的引用,而str1指向的是堆中的引用,所以輸出為false。

而StringBuilder的toString()是通過char[]創建字元串:
在这里插入图片描述
在堆中創建了abcdef之後,str2調用intern()將堆中引用放入字元串池並返回此引用,與str2指向堆中同一個字元串對象,所以輸出為true。

結語

Java中有時候很小的問題也會發散出很多知識點,不論是底層還是JVM的理論學習,結合應用案例會理解的更加深刻。就像文中提到的常量池就是class文件結構和類載入理論學習的一部分。

公众号