字串問題

JosenQiao發表於2017-11-12

程式碼段一:

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的方式生成字串有本質的區別:

image
image

  通過字面量賦值建立字串時,會優先在常量池中查詢是否已經存在相同的字串,倘若已經存在,棧中的引用直接指向該字串;倘若不存在,則在常量池中生成一個字串,再將棧中的引用指向該字串。而通過new的方式建立字串時,就直接在堆中生成一個字串的物件(備註,JDK 7 以後,HotSpot 已將常量池從永久代轉移到了堆中。詳細資訊可參考《JDK8記憶體模型-消失的PermGen》一文),棧中的引用指向該物件。對於堆中的字串物件,可以通過 intern() 方法來將字串新增的常量池中,並返回指向該常量的引用。

現在我們應該能很清楚程式碼段一的結果了:

結果 #1:因為str1指向的是字串中的常量,str2是在堆中生成的物件,所以str1==str2返回false。

結果 #2:str2呼叫intern方法,會將str2中值(“string”)複製到常量池中,但是常量池中已經存在該字串(即str1指向的字串),所以直接返回該字串的引用,因此str1==str2返回true。

以下執行程式碼段一的程式碼的結果:

image
image

程式碼段二 分析

對於程式碼段二的結果,還是通過反編譯StringTest01.class檔案比較容易理解:

常量池內容(部分):

image
image

執行指令(部分,第二列#+序數對應常量池中的項):

在解釋上述執行過程之前,先了解兩條指令:

  • 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。

執行程式碼段二,輸出結果如下:

image
image

3、程式碼段三解析:

對於程式碼段三,在 JDK 1.6 和 JDK 1.7中的執行結果不同。我們先看一下執行結果,然後再來解釋其原因:

JDK 1.6 下的執行結果:

image
image

JDK 1.7 下的執行結果:

image
image

根據對程式碼段一的分析,應該可以很簡單得出 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:falsefalsetrue,具體原因參考第二部分內容。複製程式碼

相關文章