原來學java的時候,這塊就沒怎么看,最近學多線程稍微仔細看了一下,遇到不少疑惑。
參考了這篇博客String:字符串常量池
問題一:String str1 = new String("abc"); 到底創建了幾個對象?
一般的回答
2個,一個是在堆中new的String("abc")對象,一個是字符串常量池創建的"abc"。
更嚴謹的說法
- 嚴謹的問法:
- String str1 = new String("abc"); 運行時(包括類加載和程序執行)涉及幾個String實例?
- 回答
- 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"字面量實例。
在問題三的基礎上,添上intern()方法會不會在常量池中創建"ABCABC"實例?
答案
不會。
public class Test {
public static void main(String[] args) {
String str1 = new String("ABC") + "ABC";
System.out.println(str1.intern() == str1);
}
}
結果是true,可見並沒有在常量池中創建"ABCABC"字面量實例。
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指向不同對象
}
}
總結
網絡上有很多人寫博客,但是良莠不齊,有的寫的很誤導人,而且可能有錯誤,不認真思考的話很容易掉坑里。
希望大家保持質疑的態度,多動手多思考,不要人雲亦雲。