1. 程式人生 > >從Java的字串池、常量池理解String的intern()

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

# 前言 逛知乎遇到一個剛學Java就會接觸的字串比較問題: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20201005200445906.png#pic_center) 通常,根據"**==比較的是地址,equals比較的是值**"介個定理就能得到結果。但是String有些特殊,通過new String(string)生成的兩個同值的字串地址就不相等,用其他方式來生成的兩個同值字串地址就相等。 程式碼如下: ```java // 第一種方式建立字串,字面量賦值 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"就是字面量;符號引用就是類和介面的完全限定名,欄位的名稱和描述符,方法的名稱和描述符。 如圖: ![符號引用控制代碼](https://img-blog.csdnimg.cn/20201009172837960.png#pic_center) 圖中的就是new String(String)這個方法在常量池中的名稱和描述符,即符號引用。 ### 執行時常量池 我們平時說的常量池指的就是執行時常量池。在類載入的解析階段,會將class常量池載入記憶體中(JDK1.7之前位於方法區,現在位於Heap中),並且將符號引用解析成直接引用,即根據對方法/類的描述資訊指向記憶體中對應的方法/類。執行時常量池具有動態性,可以在執行期新增新的變數進入常量池。 # intern() 先看一下intern()這個方法的描述:![intern description](https://img-blog.csdnimg.cn/20201009182141228.png#pic_center) 用二級英文水平翻譯一波,大意就是一個string呼叫intern()的時候,如果池中有和這個字串值相等的字串物件,就會將字串池中的字串物件返回;如果沒有,就將這個字串新增進去,並返回這個字串的引用。字串池由String類私有維護。 *這裡又引入了**字串池**這個概念。* ### 字串池 字串池存放的是常量池中字串物件的引用,而不是字串物件。通過第一種字面量賦值法建立的字串會放在常量池中,字串池就會儲存這個字串物件的引用,當再次在常量池建立字串時,會先從字串池檢視是否有此字串的等值引用,如果有的話,直接指向此引用對應的物件。 而第二種方式建立的字串,會在字串池中查詢是否有與構造引數等值的字串,以此決定是否需要在常量池新建字串,然後拷貝常量池中字串在Heap建立一個新的字串。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20201010113032444.png#pic_center) 如圖,在堆中會在常量池中建立一個名為original的新字串,然後拷貝並在堆中生成一個新字串。註釋中也提到,除非你需要一個字串的顯式副本,否則不需要使用這個建構函式,因為字串是不可變的。 這裡使用intern()測試一下字串池: ```java 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[]建立字串: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20201010144934482.png#pic_center) 在堆中建立了abcdef之後,str2呼叫intern()將堆中引用放入字串池並返回此引用,與str2指向堆中同一個字串物件,所以輸出為true。 # 結語 Java中有時候很小的問題也會發散出很多知識點,不論是底層還是JVM的理論學習,結合應用案例會理解的更加深刻。就像文中提到的常量池就是class檔案結構和類載入理論學習的一部分。 ![公眾號](https://img-blog.csdnimg.cn/20201217145857