從Java的字串池、常量池理解String的intern()
阿新 • • 發佈:2021-01-15
# 前言
逛知乎遇到一個剛學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