1. 程式人生 > 實用技巧 >Java 字串常量池遇到的坑

Java 字串常量池遇到的坑

原來學java的時候,這塊就沒怎麼看,會用就過去了,最近學多執行緒稍微仔細看了一下,遇到不少疑惑。
參考了這篇部落格https://segmentfault.com/a/1190000009888357

問題一:String str1 = new String("abc"); 到底建立了幾個物件?

一般的回答

2個,一個是在堆中new的String("abc")物件,一個是字串常量池建立的"abc"。

更嚴謹的說法

  1. 嚴謹的問法:
  • String str1 = new String("abc"); 執行時(包括類載入和程式執行)涉及幾個String例項?
  1. 回答
  • 2個。一個是字串字面量"abc"對應的,駐留在字串常量池的例項(類載入時建立);一個是new String("abc")在堆中建立的,內容和"abc"相同的例項(程式執行時)
 Code:
      stack=3, locals=2, args_size=1
         0: new           #2                  // class java/lang/String
         3: dup
         4: ldc           #3                  // String abc
         6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
         9: astore_1
        10: return

可以看到只有一個new,在堆中建立了String物件(Code:0:new),
"abc"字面量例項在常量池中已經存在,所以只是把先前類載入中建立好的String("abc")例項的一個引用壓入操作棧頂,並沒有建立String物件。(Code:4:ldc)

問題二:String str1 = new String("A"+"B"); 在字串常量池中建立幾個例項?

錯誤的回答

  • 3個。"A"、"B"、"AB"。

正確的回答

  • 1個。只有"AB"。

檢視字串常量池:
方法:javap -verbose XXX.class

Constant pool:
   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
   #2 = Class              #16            // java/lang/String
   #3 = String             #17            // AB
   #4 = Methodref          #2.#18         // java/lang/String."<init>":(Ljava/lang/String;)V
   #5 = Class              #19            // Test7
   #6 = Class              #20            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               Test7.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = Utf8               java/lang/String
  #17 = Utf8               AB
  #18 = NameAndType        #7:#21         // "<init>":(Ljava/lang/String;)V
  #19 = Utf8               Test7
  #20 = Utf8               java/lang/Object
  #21 = Utf8               (Ljava/lang/String;)V

可以看到只有一個"AB"。
也可以通過位元組碼看到:

Code:
      stack=3, locals=2, args_size=1
         0: new           #2                  // class java/lang/String
         3: dup
         4: ldc           #3                  // String AB
         6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
         9: astore_1
        10: return

只有一個的原因

  • 編譯時優化,會把"A"和"B"合併成一個"AB"保留到常量池。

問題三:String str1 = new String("ABC") + "ABC"; 在字串常量池中建立幾個例項?

錯誤的回答

  • 2個。"ABC"和"ABCABC"。

正確的回答

  • 1個。只有"ABC"。
 Code:
      stack=4, locals=2, args_size=1
         0: new           #2                  // class java/lang/StringBuilder
         3: dup
         4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
         7: new           #4                  // class java/lang/String
        10: dup
        11: ldc           #5                  // String ABC
        13: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        16: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: ldc           #5                  // String ABC
        21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        27: astore_1
        28: return

實際是建立了一個StringBuilder物件(Code:0:new),然後又建立了一個String物件(Code:7:new),
接著把已經駐留在常量池中的"ABC"壓入操作棧(Code:11:ldc),呼叫append方法。(重複1次)。
最後呼叫toString方法獲得合併後的String物件。
也就是說建立了2個物件,在常量池中駐留了一個"ABC"字面量例項。

在問題三的基礎上,添上str1.intern();會不會在常量池中建立"ABCABC"例項?

答案

不會。(JDK1.7及之後)

public class Test {
    public static void main(String[] args) {
        String str1 = new String("ABC") + "ABC";
        System.out.println(str1.intern() == str1);
    }
}

結果是true,可見並沒有在常量池中建立"ABCABC"字面量例項。

這裡其實是個坑,JDK1.7之後常量池移到堆中了。

intern()方法

  • 如果在常量池當中沒有字串的引用,那麼就會生成一個在常量池當中的引用,否則直接返回常量池中字串引用。

分析上面的程式碼:

public class Test {
    public static void main(String[] args) {
        String str1 = new String("ABC") + "ABC";  //str1指向堆中合併後的String("ABCABC")物件
        System.out.println(str1.intern() == str1); //intern()方法:在常量池中找不到“ABCABC”這個常量物件(問題三已經說明),所以生成常量引用,和堆中那個物件的地址相同,也就是str1
    }
}
  • 把程式碼稍作修改,intern()方法能找到"ABCABC"常量物件嗎?
public class Test {
    public static void main(String[] args) {
        String str1 = new String("ABC") + "ABC"; 
        System.out.println(str1.intern() == str1); 
        String str2 = "ABCABC";
        System.out.println(str1 == str2);
    }
}

結果是

true
true

也就是說intern()方法仍然找不到"ABCABC"常量物件,並且str2隨後在常量池中找到了"ABCABC"的引用,所以str1和str2都指向了一開始堆中合併後的String("ABCABC")物件。

  • 換一種寫法,讓intern()方法找到"ABCABC"常量物件
    String str2 = "ABCABC";移動到第一行:
public class Test {
    public static void main(String[] args) {
        String str2 = "ABCABC";
        String str1 = new String("ABC") + "ABC"; 
        System.out.println(str1.intern() == str1);
        System.out.println(str1 == str2);
    }
}

結果是:

false
false

原因:

public class Test {
    public static void main(String[] args) {
        String str2 = "ABCABC"; //在常量池建立了"ABCABC"字面量例項,str2指向該例項
        String str1 = new String("ABC") + "ABC"; //在堆中得到一個合併的String("ABCABC")物件,str1指向它
        System.out.println(str1.intern() == str1); //intern()方法在常量池能找到"ABCABC"常量物件,直接返回它的引用,也就是str2,所以str1.intern() != str1
        System.out.println(str1 == str2); //str1.intern()和str2指向同一個物件,str1和str2指向不同物件
    }
}

總結

網路上有很多人寫部落格,但是良莠不齊,有的寫的很誤導人,而且可能有錯誤,不認真思考的話很容易掉坑裡。
希望大家保持質疑的態度,多動手多思考,不要人云亦云。