java基礎:String — 字串常量池與intern(二)

Hiway發表於2018-12-16

其他更多java基礎文章: java基礎學習(目錄)


學習資料:
String類API中文
深入解析String#intern
Java 中new String("字面量") 中 "字面量" 是何時進入字串常量池的?
new一個String物件的時候,如果常量池沒有相應的字面量真的會去它那裡建立一個嗎?我表示懷疑。

通過上一篇的學習,我們已經瞭解了String原始碼的方法,這一章,我們就通過Stirng.intern()方法來延伸,講一下String的其他方面。

字串字面量

字串字面量是在 Java™語言規範的3.10.5. String 字面量中定義的 關於字面量通俗點解釋就是,使用雙引號""建立的字串,在堆中建立了物件後其引用插入到字串常量池中(jdk1.7後),可以全域性使用,遇到相同內容的字面量,就不需要再次建立。舉個例子:

//這就是建立了一個aaa字串字面量
String a = "aaa";
//簡單來說,這就是建立了一個Stirng物件和一個aaa字串字面量,後面會詳細討論
String a = new String("aaa")
複製程式碼

字串常量池

java中常量池的概念主要有三個:全域性字串常量池class檔案常量池執行時常量池。我們現在所說的就是全域性字串常量池,在下文中可能會簡稱常量池。對這個想弄明白的同學可以看這篇Java中幾種常量池的區分

字串常量池裡面存的到底是物件,還是引用呢?我查了很多資料,最後根據自己的測試和查到的各種說法,認為在jdk1.7後字串常量池中存的是引用。在new一個String物件的時候,如果常量池沒有相應的字面量真的會去它那裡建立一個嗎?我表示懷疑。問題中,R大的回答解答了我:

至於說: 之前一直有個結論就是:當建立一個string物件的時候,去字串常量池看是否有相應的字面量,如果沒有就建立一個。 這個說法從來都不正確。 物件在堆裡。常量池存引用。

這個字串常量池的位置也是隨著jdk版本的不同而位置不同。在jdk6中,常量池的位置在永久代(方法區)中,此時常量池中儲存的是物件。在jdk7中,常量池的位置在堆中,此時,常量池儲存的就是引用了。在jdk8中,永久代(方法區)被元空間取代了。這裡就引出了一個很常見很經典的問題,看下面這段程式碼。

    @Test
    public void test(){
        String s = new String("2");
        s.intern();
        String s2 = "2";
        System.out.println(s == s2);


        String s3 = new String("3") + new String("3");
        s3.intern();
        String s4 = "33";
        System.out.println(s3 == s4);
    }

jdk6
false
false

jdk7
false
true
複製程式碼

這段程式碼在jdk6中輸出是false false,但是在jdk7中輸出的是false true。我們通過圖來一行行解釋。

JDK1.6程式碼圖

JDK1.6
String s = new String("2");建立了兩個物件,一個在堆中的StringObject物件,一個是在常量池中的“2”物件。
s.intern();在常量池中尋找與s變數內容相同的物件,發現已經存在內容相同物件“2”,返回物件2的地址。
String s2 = "2";使用字面量建立,在常量池尋找是否有相同內容的物件,發現有,返回物件"2"的地址。
System.out.println(s == s2);從上面可以分析出,s變數和s2變數地址指向的是不同的物件,所以返回false

String s3 = new String("3") + new String("3");建立了兩個物件,一個在堆中的StringObject物件,一個是在常量池中的“3”物件。中間還有2個匿名的new String("3")我們不去討論它們。
s3.intern();在常量池中尋找與s3變數內容相同的物件,沒有發現“33”物件,在常量池中建立“33”物件,返回“33”物件的地址。
String s4 = "33";使用字面量建立,在常量池尋找是否有相同內容的物件,發現有,返回物件"33"的地址。
System.out.println(s3 == s4);從上面可以分析出,s3變數和s4變數地址指向的是不同的物件,所以返回false

JDK1.7程式碼圖

JDK1.7
String s = new String("2");建立了兩個物件,一個在堆中的StringObject物件,一個是在堆中的“2”物件,並在常量池中儲存“2”物件的引用地址。
s.intern();在常量池中尋找與s變數內容相同的物件,發現已經存在內容相同物件“2”,返回物件“2”的引用地址。
String s2 = "2";使用字面量建立,在常量池尋找是否有相同內容的物件,發現有,返回物件“2”的引用地址。
System.out.println(s == s2);從上面可以分析出,s變數和s2變數地址指向的是不同的物件,所以返回false

String s3 = new String("3") + new String("3");建立了兩個物件,一個在堆中的StringObject物件,一個是在堆中的“3”物件,並在常量池中儲存“3”物件的引用地址。中間還有2個匿名的new String("3")我們不去討論它們。
s3.intern();在常量池中尋找與s3變數內容相同的物件,沒有發現“33”物件,將s3對應的StringObject物件的地址儲存到常量池中,返回StringObject物件的地址。
String s4 = "33";使用字面量建立,在常量池尋找是否有相同內容的物件,發現有,返回其地址,也就是StringObject物件的引用地址。
System.out.println(s3 == s4);從上面可以分析出,s3變數和s4變數地址指向的是相同的物件,所以返回true。

再來一段變種程式碼
通過上面的逐句分析,應該都瞭解了為什麼兩個版本的jdk返回值會不一樣了。那我們稍稍改變一下上面程式碼中的語句順序,將intern方法與字面量賦值語句調換順序:

        String s = new String("2");
        String s2 = "2";
        s.intern();
        System.out.println(s == s2);

        String s3 = new String("3") + new String("3");
        String s4 = "33";
        s3.intern();
        System.out.println(s3 == s4);
複製程式碼

答案是多少呢,大家可以稍微思考一下再往下看:

jdk6
false
false

jdk7
false
false
複製程式碼

原理很簡單,因為在呼叫intern方法前,先使用了字面量賦值語句,所以在常量池中都存在了與變數相同內容的物件(jdk1.6)或物件的引用(jdk1.7+),此時再呼叫intern方法,就會發現常量池裡的物件地址和變數的地址不是指向同一個物件,自然就false了。對於這段不懂的同學可以評論,我看需不需要再畫一次結構圖和逐句解釋。

字面量是何時進入常量池

通過上面兩段程式碼,我們發現呼叫intern方法和字面量賦值的順序是很重要的。我們將上面兩段程式碼都通過javap命令檢視其位元組碼,發現在class類常量池中都有“33”。這說明在執行時,class常量池裡的常量並不會直接全部加入到全域性常量池中,那這是在什麼時候加入的呢?我搜到了下面大神的回答 new String(“字面量”) 中 “字面量” 是何時進入字串常量池的?

簡單來說:

  • HotSpot VM的實現來說,載入類的時候,那些字串字面量會進入到當前類的執行時常量池,不會進入全域性的字串常量池 ;

  • 在字面量賦值的時候,會翻譯成位元組碼ldc指令,ldc指令觸發lazy resolution動作

    • 到當前類的執行時常量池(runtime constant pool,HotSpot VM裡是ConstantPool + ConstantPoolCache)去查詢該index對應的項
    • 如果該項尚未resolve則resolve之,並返回resolve後的內容。
    • 在遇到String型別常量時,resolve的過程如果發現StringTable已經有了內容匹配的java.lang.String的引用,則直接返回這個引用;
    • 如果StringTable裡尚未有內容匹配的String例項的引用,則會在Java堆裡建立一個對應內容的String物件,然後在StringTable記錄下這個引用,並返回這個引用出去。

String“+”符號的實現

在我們使用中經常會用到+符號來拼接字串,但是這個+符號在String中的實現還是有講究的。如果是相加含有String物件,則底部是使用StringBuilder實現的拼接的

String str1 ="str1";
String str2 ="str2";
String str3 = str1 + str2;
複製程式碼

如果相加的引數只有字面量或者常量或基礎型別變數,則會直接編譯為拼接後的字串。

String str1 =1+"str2"+"str3"複製程式碼

這裡有個小細節
如果使用字面量拼接的話,java常量池裡是不會儲存拼接的引數的,而是直接編譯成拼接後的字串儲存,我們看看這段程式碼:

        String str1 = new String("aa"+"bb");
        //String str3 = "aa";
        String str2 = new StringBuilder("a").append("a").toString();
        System.out.println(str2==str2.intern());
複製程式碼

這段程式碼的輸出是true。可以得知,在str1變數的建立中,雖然我們用了字面量“aa”,但是我們常量池裡並沒有aa,所以str2==str.intern()才會返回true。如果我們去掉str3的註釋,重新執行,就會輸出false

個人疑問

我在學習的過程中,遇到了一個疑問,怎麼都查不到是為什麼,大家如果看到這裡,可以順手寫一下這段程式碼,看是不是也會遇到這樣的問題。

public static void main(String[] args){
        String s3 = new String("1") + new String("1");
        s3.intern();
        String s4 = "11";
        System.out.println(s3 == s4);
    }
複製程式碼
   @Test
    public void test7(){
        String s3 = new String("1") + new String("1");
        s3.intern();
        String s4 = "11";
        System.out.println(s3 == s4);
    }
複製程式碼

如上所示,分別在test環境和main方法裡執行相同程式碼,此時main函式裡返回true,test環境下卻是返回false。按邏輯這裡應該是返回true才對。但是我測試了將引數“1”改為“2“”或者“3”,兩者返回的都是true。

相關文章