第一種情況
/* * 第一種情況 * 證明:是否在編譯的時候完成拼接 * */ String str = "a" + "b";
常量池信息:
查看常量池信息必須通過 javap -v 命令來查看Class文件(java文件編譯后的文件)
Constant pool: #1 = Methodref #4.#20 // java/lang/Object."<init>":()V #2 = String #21 // ab #3 = Class #22 // com/test/StringTest2 #4 = Class #23 // java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Utf8 Code #8 = Utf8 LineNumberTable #9 = Utf8 LocalVariableTable #10 = Utf8 this #11 = Utf8 Lcom/test/StringTest2; #12 = Utf8 main #13 = Utf8 ([Ljava/lang/String;)V #14 = Utf8 args #15 = Utf8 [Ljava/lang/String; #16 = Utf8 str #17 = Utf8 Ljava/lang/String; #18 = Utf8 SourceFile #19 = Utf8 StringTest2.java #20 = NameAndType #5:#6 // "<init>":()V #21 = Utf8 ab #22 = Utf8 com/test/StringTest2 #23 = Utf8 java/lang/Object
可以看到 #21 是字符串"ab"且沒有單獨的 "a" 和 "b",說明String str = "a" + "b" 在編譯時期就已經拼接在一塊了,效果與 String str = "ab" 相同。
進一步說明,第一種情況只在字符串常量池中創建了一個"ab"對象,沒有"a"對象和"b"對象。(類加載過程中Class文件的常量池內容會進入字符串常量池)。
第二種情況
/* * 第二種情況 * 證明:是否是在編譯的時候完成拼接,如果不是,那么是按照什么方式進行的拼接。 * */ String str = "a"; String str1 = "b"; String str3 = str + str1;
常量池信息
Constant pool: #1 = Methodref #9.#27 // java/lang/Object."<init>":()V #2 = String #28 // a #3 = String #29 // b #4 = Class #30 // java/lang/StringBuilder #5 = Methodref #4.#27 // java/lang/StringBuilder."<init>":()V #6 = Methodref #4.#31 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #7 = Methodref #4.#32 // java/lang/StringBuilder.toString:()Ljava/lang/String; #8 = Class #33 // com/test/StringTest2 #9 = Class #34 // java/lang/Object #10 = Utf8 <init> #11 = Utf8 ()V #12 = Utf8 Code #13 = Utf8 LineNumberTable #14 = Utf8 LocalVariableTable #15 = Utf8 this #16 = Utf8 Lcom/test/StringTest2; #17 = Utf8 main #18 = Utf8 ([Ljava/lang/String;)V #19 = Utf8 args #20 = Utf8 [Ljava/lang/String; #21 = Utf8 str #22 = Utf8 Ljava/lang/String; #23 = Utf8 str1 #24 = Utf8 str3 #25 = Utf8 SourceFile #26 = Utf8 StringTest2.java #27 = NameAndType #10:#11 // "<init>":()V #28 = Utf8 a #29 = Utf8 b #30 = Utf8 java/lang/StringBuilder #31 = NameAndType #35:#36 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #32 = NameAndType #37:#38 // toString:()Ljava/lang/String; #33 = Utf8 com/test/StringTest2 #34 = Utf8 java/lang/Object #35 = Utf8 append #36 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #37 = Utf8 toString #38 = Utf8 ()Ljava/lang/String;
從常量池中我們可以看到 #28 和 #29 分別為字符串 "a" 和 "b",並且不存在字符串 "ab",所以可以排除第二種情況是在編譯期完成的拼接。
那么不是編譯器完成拼接那么是通過什么方式進行拼接的呢?我們可以通過查看反匯編指令,來進一步了解拼接的執行過程。
反匯編指令可以通過 javap -v 或者 java -c 查看Class文件(java編譯后的文件)得到。
stack=2, locals=4, args_size=1 0: ldc #2 // String a 2: astore_1 3: ldc #3 // String b 5: astore_2 6: new #4 // class java/lang/StringBuilder 9: dup 10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V 13: aload_1 14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 17: aload_2 18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 24: astore_3 25: return
反匯編指令詳解可以百度,這里不做過多解釋
我們可以看到 第6行new了一個StringBuilder對象,之后執行了兩次append()方法,把字符串 "a" 和 "b" 拼接到一塊,然后通過StringBuilder.toString()方法返回一個String對象,該String 對象就是完成拼接后的對象。
這也意味着,通過這種方式將來在類加載的過程字符串常量池中只會保存 "a" 和 "b" 這兩個對象,而沒有保存 "ab" 這個對象。
當然從編譯角度看,編譯時期無法識別變量的具體值的,所以 "ab" 這個字符串自然也不會保存到Class文件的常量池中。
這里再提一點:StringBuilder.toString()方法返回的是一個拼接后的String對象,如果字符串很長的時候,盡量不要多次使用StringBuilder.toString()方法,否則會浪費空間甚至造成內存溢出。
第三種情況
/* * 第三種情況 * 與第二種差不多 * */ String str = new String("a") + new String("b");
常量池:
Constant pool: #1 = Methodref #11.#27 // java/lang/Object."<init>":()V #2 = Class #28 // java/lang/StringBuilder #3 = Methodref #2.#27 // java/lang/StringBuilder."<init>":()V #4 = Class #29 // java/lang/String #5 = String #30 // a #6 = Methodref #4.#31 // java/lang/String."<init>":(Ljava/lang/String;)V #7 = Methodref #2.#32 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #8 = String #33 // b #9 = Methodref #2.#34 // java/lang/StringBuilder.toString:()Ljava/lang/String; #10 = Class #35 // com/test/StringTest2 #11 = Class #36 // java/lang/Object #12 = Utf8 <init> #13 = Utf8 ()V #14 = Utf8 Code #15 = Utf8 LineNumberTable #16 = Utf8 LocalVariableTable #17 = Utf8 this #18 = Utf8 Lcom/test/StringTest2; #19 = Utf8 main #20 = Utf8 ([Ljava/lang/String;)V #21 = Utf8 args #22 = Utf8 [Ljava/lang/String; #23 = Utf8 str #24 = Utf8 Ljava/lang/String; #25 = Utf8 SourceFile #26 = Utf8 StringTest2.java #27 = NameAndType #12:#13 // "<init>":()V #28 = Utf8 java/lang/StringBuilder #29 = Utf8 java/lang/String #30 = Utf8 a #31 = NameAndType #12:#37 // "<init>":(Ljava/lang/String;)V #32 = NameAndType #38:#39 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #33 = Utf8 b #34 = NameAndType #40:#41 // toString:()Ljava/lang/String; #35 = Utf8 com/test/StringTest2 #36 = Utf8 java/lang/Object #37 = Utf8 (Ljava/lang/String;)V #38 = Utf8 append #39 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #40 = Utf8 toString #41 = Utf8 ()Ljava/lang/String;
反匯編
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 a 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: new #4 // class java/lang/String 22: dup 23: ldc #8 // String b 25: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V 28: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 31: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 34: astore_1 35: return
第三種情況和第二種情況不同點:第三種情況會在堆里面多創建兩個對象,其它的都一樣
———————————————————————————————————————————————————————————————————————————————————————————————————
下面再介紹一下String類型的intern()方法,在jdk1.8中 intern()方法會先去判斷字符串常量池中是否存在想要查找的字符串對象,如果存在則返回字符串常量池中對象的地址;如果不存在則返回當前對象(誰調用這個方法,誰就是當前對象)的引用,並且在字符串常量池中創建指向當前對象的引用。注:jdk 1.6與jdk1.8有所不同,有興趣的可以了解下jdk1.6的intern()方法。
舉個例子:
String str = new String("ab"); System.out.println(str.intern()==str); //false String str1 = new String("c") + new String("d"); System.out.println(str1.intern()==str1); //true
第一種情況,會同時在字符串常量池和堆空間中各生成一個對象,所以str.intern返回的是字符串常量池中 "ab" 的地址,str返回的是堆空間中 "ab" 的地址。
第二種情況,只會在堆空間中生成一個對象,所以str1.intern()返回的是str1對象即堆空間對象的地址,並且會在字符串常量池中創建一個指向堆空間對象的引用。
那么也就是說,如果創建一個引用指向 "cd",比如 String str2 = "cd",那么str1.intern()==str2 也是成立的,但第一種情況就不成立。
面試題:
String str = new String("ab"); String str1 = new String("ab"); String str2 = "ab"; String str3 = "ab"; System.out.println(str==str1); //false System.out.println(str2==str3); //true System.out.println(str2==str); //false System.out.println(str.intern()==str2); //true System.out.println(str.intern()==str1.intern()); //true
String str = "a"; String str1 = "b"; String str2 = str + str1; String str3 = "ab"; String str4 = str2.intern(); //str4在str3之前定義,則結果為兩個true. System.out.println(str2 == str3);//flase System.out.println(str2 == str4);//false
//問:創建幾個對象 //答:最明顯的字符串常量池分別創建了 “a”和"b",堆空間創建了 "a"和"b",然后字符串拼接會創建StringBuilder對象, // StringBuilder對象會調用toString()方法產生拼接后的String對象,所以總共創建了6個對象 String str = new String("a") + new String("b");
如果有大佬發現不正確的地方,歡迎指正,我會第一時間修改。