程式碼段一:
package com.paddx.test.string;
public class StringTest {
public static void main(String[] args) {
String str1 = "string";
String str2 = new String("string");
String str3 = str2.intern();
System.out.println(str1==str2);//#1
System.out.println(str1==str3);//#2
}
}複製程式碼
程式碼段二:
package com.paddx.test.string;
public class StringTest01 {
public static void main(String[] args) {
String baseStr = "baseStr";
final String baseFinalStr = "baseStr";
String str1 = "baseStr01";
String str2 = "baseStr"+"01";
String str3 = baseStr + "01";
String str4 = baseFinalStr+"01";
String str5 = new String("baseStr01").intern();
System.out.println(str1 == str2);//#3
System.out.println(str1 == str3);//#4
System.out.println(str1 == str4);//#5
System.out.println(str1 == str5);//#6
}
}複製程式碼
程式碼段三(1):
package com.paddx.test.string;
public class InternTest {
public static void main(String[] args) {
String str2 = new String("str")+new String("01");
str2.intern();
String str1 = "str01";
System.out.println(str2==str1);//#7
}
}複製程式碼
程式碼段三(2):
package com.paddx.test.string;
public class InternTest01 {
public static void main(String[] args) {
String str1 = "str01";
String str2 = new String("str")+new String("01");
str2.intern();
System.out.println(str2 == str1);//#8
}
}複製程式碼
為了方便描述,我對上述程式碼的輸出結果由#1~#8進行了編碼,下文中藍色字型部分即為結果。
字串深入分析
程式碼段一 分析
字串不屬於基本型別,但是可以像基本型別一樣,直接通過字面量賦值,當然也可以通過new來生成一個字串物件。不過通過字面量賦值的方式和new的方式生成字串有本質的區別:
通過字面量賦值建立字串時,會優先在常量池中查詢是否已經存在相同的字串,倘若已經存在,棧中的引用直接指向該字串;倘若不存在,則在常量池中生成一個字串,再將棧中的引用指向該字串。而通過new的方式建立字串時,就直接在堆中生成一個字串的物件(備註,JDK 7 以後,HotSpot 已將常量池從永久代轉移到了堆中。詳細資訊可參考《JDK8記憶體模型-消失的PermGen》一文),棧中的引用指向該物件。對於堆中的字串物件,可以通過 intern() 方法來將字串新增的常量池中,並返回指向該常量的引用。
現在我們應該能很清楚程式碼段一的結果了:
結果 #1:因為str1指向的是字串中的常量,str2是在堆中生成的物件,所以str1==str2返回false。
結果 #2:str2呼叫intern方法,會將str2中值(“string”)複製到常量池中,但是常量池中已經存在該字串(即str1指向的字串),所以直接返回該字串的引用,因此str1==str2返回true。
以下執行程式碼段一的程式碼的結果:
程式碼段二 分析
對於程式碼段二的結果,還是通過反編譯StringTest01.class檔案比較容易理解:
常量池內容(部分):
執行指令(部分,第二列#+序數對應常量池中的項):
在解釋上述執行過程之前,先了解兩條指令:
ldc:Push item from run-time constant pool,從常量池中載入指定項的引用到棧。
astore_:Store reference into local variable,將引用賦值給第n個區域性變數。
現在我們開始解釋程式碼段二的執行過程:
0: ldc #2:載入常量池中的第二項("baseStr")到棧中。
2: astore_1 :將1中的引用賦值給第一個區域性變數,即String baseStr = "baseStr";
3: ldc #2:載入常量池中的第二項("baseStr")到棧中。
5: astore_2 :將3中的引用賦值給第二個區域性變數,即 final String baseFinalStr="baseStr";
6: ldc #3:載入常量池中的第三項("baseStr01")到棧中。
8: astore_3 :將6中的引用賦值給第三個區域性變數,即String str1="baseStr01";
9: ldc #3:載入常量池中的第三項("baseStr01")到棧中。
11: astore 4:將9中的引用賦值給第四個區域性變數:即String str2="baseStr01";
結果#3:str1==str2 肯定會返回true,因為str1和str2都指向常量池中的同一引用地址。所以其實在JAVA 1.6之後,常量字串的“+”操作,編譯階段直接會合成為一個字串。
13: new #4:生成StringBuilder的例項。
16: dup :複製13生成物件的引用並壓入棧中。
17: invokespecial #5:呼叫常量池中的第五項,即StringBuilder.<init>方法。
以上三條指令的作用是生成一個StringBuilder的物件。
20: aload_1 :載入第一個引數的值,即"baseStr"
21: invokevirtual #6 :呼叫StringBuilder物件的append方法。
24: ldc #7:載入常量池中的第七項("01")到棧中。
26: invokevirtual #6:呼叫StringBuilder.append方法。
29: invokevirtual #8:呼叫StringBuilder.toString方法。
32: astore 5:將29中的結果引用賦值改第五個區域性變數,即對變數str3的賦值。
結果 #4:因為str3實際上是stringBuilder.append()生成的結果,所以與str1不相等,結果返回false。
34: ldc #3:載入常量池中的第三項("baseStr01")到棧中。
36: astore 6:將34中的引用賦值給第六個區域性變數,即str4="baseStr01";
結果 #5 :因為str1和str4指向的都是常量池中的第三項,所以str1==str4返回true。這裡我們還能發現一個現象,對於final欄位,編譯期直接進行了常量替換,而對於非final欄位則是在執行期進行賦值處理的。
38: new #9:建立String物件
41: dup :複製引用並壓如棧中。
42: ldc #3:載入常量池中的第三項("baseStr01")到棧中。
44: invokespecial #10:呼叫String."<init>"方法,並傳42步驟中的引用作為引數傳入該方法。
47: invokevirtual #11:呼叫String.intern方法。
從38到41的對應的原始碼就是new String("baseStr01").intern()。
50: astore 7:將47步返回的結果賦值給變數7,即str5指向baseStr01在常量池中的位置。複製程式碼
結果 #6 :因為str5和str1都指向的都是常量池中的同一個字串,所以str1==str5返回true。
執行程式碼段二,輸出結果如下:
3、程式碼段三解析:
對於程式碼段三,在 JDK 1.6 和 JDK 1.7中的執行結果不同。我們先看一下執行結果,然後再來解釋其原因:
JDK 1.6 下的執行結果:
JDK 1.7 下的執行結果:
根據對程式碼段一的分析,應該可以很簡單得出 JDK 1.6 的結果,因為 str2 和 str1本來就是指向不同的位置,理應返回false。
比較奇怪的問題在於JDK 1.7後,對於第一種情況返回true,但是調換了一下位置返回的結果就變成了false。這個原因主要是從JDK 1.7後,HotSpot 將常量池從永久代移到了元空間,正因為如此,JDK 1.7 後的intern方法在實現上發生了比較大的改變,JDK 1.7後,intern方法還是會先去查詢常量池中是否有已經存在,如果存在,則返回常量池中的引用,這一點與之前沒有區別,區別在於,如果在常量池找不到對應的字串,則不會再將字串拷貝到常量池,而只是在常量池中生成一個對原字串的引用。所以:
==結果 #7:在第一種情況下,因為常量池中沒有“str01”這個字串,所以會在常量池中生成一個對堆中的“str01”的引用,而在進行字面量賦值的時候,常量池中已經存在,所以直接返回該引用即可,因此str1和str2都指向堆中的字串,返回true。==
==結果 #8:調換位置以後,因為在進行字面量賦值(String str1 = "str01")的時候,常量池中不存在,所以str1指向的常量池中的位置,而str2指向的是堆中的物件,再進行intern方法時,對str1和str2已經沒有影響了,所以返回false。==
三、常見面試題解答
有了對以上的知識的瞭解,我們現在再來看常見的面試或筆試題就很簡單了:
Q:String s = new String("xyz"),建立了幾個String Object?
A:兩個,常量池中的"xyz"和堆中物件。
Q:下列程式的輸出結果:
String s1 = “abc”;
String s2 = “abc”;
System.out.println(s1 == s2);
A:true,均指向常量池中物件。
Q:下列程式的輸出結果:
String s1 = new String(“abc”);
String s2 = new String(“abc”);
System.out.println(s1 == s2);
A:false,兩個引用指向堆中的不同物件。
Q:下列程式的輸出結果:
String s1 = “abc”;
String s2 = “a”;
String s3 = “bc”;
String s4 = s2 + s3;
System.out.println(s1 == s4);
A:false,因為s2+s3實際上是使用StringBuilder.append來完成,會生成不同的物件。
Q:下列程式的輸出結果:
String s1 = “abc”;
final String s2 = “a”;
final String s3 = “bc”;
String s4 = s2 + s3;
System.out.println(s1 == s4);
A:true,因為final變數在編譯後會直接替換成對應的值,所以實際上等於s4="a"+"bc",而這種情況下,編譯器會直接合併為s4="abc",所以最終s1==s4。
Q:下列程式的輸出結果:
String s = new String("abc");
String s1 = "abc";
String s2 = new String("abc");
System.out.println(s == s1.intern());
System.out.println(s == s2.intern());
System.out.println(s1 == s2.intern());
A:false,false,true,具體原因參考第二部分內容。複製程式碼