1. 程式人生 > >關於String str1="123"與String str2=new String("123")類問題詳解

關於String str1="123"與String str2=new String("123")類問題詳解

要徹底弄明白這個問題,我們需要清楚一些基本概念:Class檔案中的常量池,執行時常量池(runtime constant pool),全域性字串常量池(StringTable),Java heap,一些常用位元組碼以及常量池中的常量型別等jvm的知識:

Class檔案常量池:JVM會為我們每個類對應生成一個常量池,常量池可以理解為Class檔案之中的資源倉庫,它是Class檔案結構中與其他專案關聯最多的資料型別,也是佔用Class檔案空間最大的資料專案之一,同時它還是在Class檔案中第一個出現的表型別資料專案。Class檔案被載入之後,Class檔案常量池就變成了執行時常量池。

全域性字串常量池: HotSpot VM裡,記錄interned string的一個全域性表叫做StringTable,它本質上就是個HashSet<String>。這是個純執行時的結構,而且是惰性(lazy)維護的。注意它只儲存對java.lang.String例項的引用,而不儲存String物件的內容。 注意,它只存了引用,根據這個引用可以得到具體的String物件。

Java heap:對大多數應用來說,Java heap是JVM所管理的記憶體中最大的一塊,是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。此記憶體區域的唯一作用即使存放物件,幾乎所有物件例項都在這裡分配(隨著JIT編譯器的發展與逃逸分析技術逐漸成熟,所有物件都在堆上分配變得不那麼絕對了)。 

概念介紹完畢,進入正題,先來看一段程式碼:

String str1 = new String("12");
String str2 = "12";
System.out.println(str2 == str1);
	

這段程式碼返回結果是false。 

以下是位元組碼和常量池內容:

Constant pool:
   #1 = Class              #2             // CodeTest/StringTest
   #2 = Utf8               CodeTest/StringTest
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Methodref          #3.#9          // java/lang/Object."<init>":()V
   #9 = NameAndType        #5:#6          // "<init>":()V
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               LCodeTest/StringTest;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Class              #17            // java/lang/String
  #17 = Utf8               java/lang/String
  #18 = String             #19            // 12
  #19 = Utf8               12
  #20 = Methodref          #16.#21        // java/lang/String."<init>":(Ljava/lang/String;)V
  #21 = NameAndType        #5:#22         // "<init>":(Ljava/lang/String;)V
  #22 = Utf8               (Ljava/lang/String;)V
  #23 = Fieldref           #24.#26        // java/lang/System.out:Ljava/io/PrintStream;
  #24 = Class              #25            // java/lang/System
  #25 = Utf8               java/lang/System
  #26 = NameAndType        #27:#28        // out:Ljava/io/PrintStream;
  #27 = Utf8               out
  #28 = Utf8               Ljava/io/PrintStream;
  #29 = Methodref          #30.#32        // java/io/PrintStream.println:(Z)V
  #30 = Class              #31            // java/io/PrintStream
  #31 = Utf8               java/io/PrintStream
  #32 = NameAndType        #33:#34        // println:(Z)V
  #33 = Utf8               println
  #34 = Utf8               (Z)V
  #35 = Utf8               args
  #36 = Utf8               [Ljava/lang/String;
  #37 = Utf8               str1
  #38 = Utf8               Ljava/lang/String;
  #39 = Utf8               str2
  #40 = Utf8               StackMapTable
  #41 = Class              #36            // "[Ljava/lang/String;"
  #42 = Utf8               SourceFile
  #43 = Utf8               StringTest.java
         0: new           #16                 // class java/lang/String
         3: dup
         4: ldc           #18                 // String 12
         6: invokespecial #20                 // Method java/lang/String."<init>":(Ljava/lang/String;)V
         9: astore_1
        10: ldc           #18                 // String 12
        12: astore_2
        13: getstatic     #23                 // Field java/lang/System.out:Ljava/io/PrintStream;
        16: aload_2
        17: aload_1
        18: if_acmpne     25
        21: iconst_1
        22: goto          26
        25: iconst_0
        26: invokevirtual #29                 // Method java/io/PrintStream.println:(Z)V
        29: return

 注意:new位元組碼出現幾次就代表建立了多少對應例項。在JVM裡,“new”位元組碼指令只負責把例項創建出來(包括分配空間、設定型別、所有欄位設定預設值等工作),並且把指向新建立物件的引用壓到運算元棧頂。此時該引用還不能直接使用,處於未初始化狀態(uninitialized);如果某方法a含有程式碼試圖通過未初始化狀態的引用來呼叫任何例項方法,那麼方法a會通不過JVM的位元組碼校驗,從而被JVM拒絕執行。 能對未初始化狀態的引用做的唯一一種事情就是通過它呼叫例項構造器,在Class檔案層面表現為特殊初始化方法“<init>”。實際呼叫的指令是invokespecial,而在實際呼叫前要把需要的引數按順序壓到運算元棧上。在上面的位元組碼例子中,壓引數的指令包括dup和ldc兩條,分別把隱藏引數(新建立的例項的引用,對於例項構造器來說就是“this”)與顯式宣告的第一個實際引數("12"常量的引用)壓到運算元棧上。 在構造器返回之後,新建立的例項的引用就可以正常使用了。 

 String str1 = new String("12"):new作為類初始化條件之一在這裡出現,首先去建立一個例項,會然後呼叫String型別的構造器,在堆中建立一個物件,並將指向該物件的引用賦給str1。此時常量池中是存在一個UTF-8縮略編碼的字串“12”的,並且在字串池中駐留引用。

String str2 = "12":str2指向的是常量池中的“12”。

所以結果會返回false。

我們修改一下程式碼:

String str1 = new String("12");
str1 = str1.intern();
String str2 = "12";
System.out.println(str2 == str1);

結果返回true,str1 = str1.intern()做了什麼,根據先前結論我們可以做出以下分析:首先,str1.intern()將指向常量池中的“12”的引用賦值給str1(先前str1指向的是堆中的物件),接下來是String str2 = "12",由於常量池已經存在一個指向“12”的引用,所有以現在將該引用賦給str2,結果返回true。

好的,接下來看這兩段程式碼:

String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
 
String s3 = new String("1") + new String("2");
s3.intern();
String s4 = "12";
System.out.println(s3 == s4);

它們的結果分別為false和true。

第一段程式碼前面已經解釋過,看第二段:

一樣的,要先用ldc把"1"和"2"送到棧頂,換句話說,會建立他倆的物件,並且會儲存引用到字串常量池中;然後有個+號對吧,內部是建立了一個StringBuilder物件,一路append,最後呼叫StringBuilder物件的toString方法得到一個String物件(內容是12,注意這個toString方法會new一個String物件),並把它賦值給s3。注意啊,沒有把12的引用放入字串常量池。接下來intern方法一看,字串常量池裡面沒有,它會把上面的這個hello物件的引用儲存到字串常量池,然後返回這個引用,但是這個返回值我們並沒有使用變數去接收,所以沒用,並且此時常量池中不存在“11”字串,JDK1.6的做法是直接在常量池中生成一個 "11" 的物件。但是在JDK1.7及以上中,常量池中不需要再儲存一份物件了,可以直接儲存堆中的引用。這份引用直接指向 s3 引用的物件,也就是說s3.intern() ==s3會返回true。String s4 = "11", 這一行程式碼會直接去常量池中建立,但是發現已經有這個物件了,此時也就是指向 s3 引用物件的一個引用。因此s3 == s4返回了true。