1. 程式人生 > 實用技巧 >深入理解String與new String

深入理解String與new String

前言

字串是最常用的型別之一,趁此機會準備探索下它的原始碼。有關該類的註釋作一個總結:

String-字串,是個常量,它們被建立後其值就不允許被改變,由於它是不可變的,所以它們可以被共享,在內部提供了多個方法來操作字串。

探索之前我曾看過其他人寫的有關於此的文章,發現JDK1.7版本前後的記憶體模型不一樣,而這部分的內容還沒有排上行程,簡單來說,我還不懂...所以不敢妄下結論,這篇文章的內容也不會從這方面展開來講,此次探索是基於JDK1.8

原理

字串是存放在常量池中,至於什麼是常量池不準備討論它。先來個簡單的示例方便分析:


    public class TestString {

        public static void main(String[] args) {
            String testOne = new String("testOne");
        }
    }

示例就是這麼簡單...先讓我們來看看僅僅是這麼一句話,而它的底層都做了什麼。緊接著反編譯它的位元組碼檔案,命令是javap -v TestString,輸出的資訊內容比較多,咱們只要常量池的內容,如圖所示:

實際上我也看的不是很懂,但有幾個點咱們是能確定的,也就是說常量池中有testOne字串,而此時的位元組碼檔案是編譯而來的,並未實際上的執行,所以該字串物件在堆記憶體中還未建立,這就說明了在編譯階段時字串就存在於常量池中,同時也驗證了一點:常量池中的字串物件與執行時在堆記憶體中建立的字串物件完全是兩個物件,因為在編譯時期堆記憶體中的物件還未出生呢,所以不可能引用到它...不知道我講明白了沒有。接著我們從另外一個角度去分析這段示例,因為後續的其他示例可能會羞澀難懂,所以先從簡單的示例開始逐漸熟悉起來,最終也是希望讀者能夠理解這方面的知識。


    Constant pool:
  
// 編號  型別                值              註釋            --------> 這裡的註釋不一定對,只是作一個標記方便理解        
    #1 = Methodref          #6.#22         // java/lang/Object."<init>":()V
    #2 = Class              #23            // java/lang/String
    #3 = String             #18            // testOne
    #4 = Methodref          #2.#24         // java/lang/String."<init>":(Ljava/lang/String;)V
    #5 = Class              #25            // test20190820/TestString
    #6 = Class              #26            // java/lang/Object
    #7 = Utf8               <init>
    #8 = Utf8               ()V
    #9 = Utf8               Code
    #10 = Utf8               LineNumberTable
    #11 = Utf8               LocalVariableTable
    #12 = Utf8               this
    #13 = Utf8               Ltest20190820/TestString;
    #14 = Utf8               main
    #15 = Utf8               ([Ljava/lang/String;)V
    #16 = Utf8               args
    #17 = Utf8               [Ljava/lang/String;
    #18 = Utf8               testOne
    #19 = Utf8               Ljava/lang/String;
    #20 = Utf8               SourceFile
    #21 = Utf8               TestString.java
    #22 = NameAndType        #7:#8          // "<init>":()V
    #23 = Utf8               java/lang/String
    #24 = NameAndType        #7:#27         // "<init>":(Ljava/lang/String;)V
    #25 = Utf8               test20190820/TestString
    #26 = Utf8               java/lang/Object
    #27 = Utf8               (Ljava/lang/String;)V

執行javap -c TestString獲取底層程式碼執行邏輯,如圖所示:


    public class test20190820.TestString {
        public test20190820.TestString(); //這裡是構造器的執行邏輯,咱們忽略它,畢竟不是重點
            Code:
            0: aload_0
            1: invokespecial #1                  // Method java/lang/Object."<init>":()V
            4: return

    public static void main(java.lang.String[]); //下面的內容涉及到位元組碼的指令,網上很多資料,可自行查閱
        Code:
        // #2:對應常量池中編號#2,加上註釋我們得出這裡是建立字串物件  -> new String();
        0: new           #2                  // class java/lang/String
        // dup:複製0序號中的引用並壓入棧中  -> 也就是將字串物件的引用放入到棧中
        3: dup
        // ldc:從常量池中載入指定項的引用到棧  #3:同上,對應著常量池的編號#3 -> 將"testOne"字串的引用載入到棧中
        4: ldc           #3                  // String testOne
        // invokespecial:初始化常量池中的指定項  -> 呼叫字串物件的初始化並將4序號中的引用作為引數傳入,形成new String("testOne")
        6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        // astore_n:將引用賦值給第n個區域性變數  -> 將6序號的引用賦值給第一個區域性變數,String testOne = new String("testOne"),jvm中是有記錄區域性變數的資訊
        9: astore_1
        // 退出方法的標誌
        10: return
    }

從上面的分析可以知道:常量池中的字串物件與在堆記憶體中建立的字串物件是兩個物件,這一點很重要!接下來是各種示例,我們將採用上面的方式進行分析。

示例一


    public class TestString {

        public static void main(String[] args) {
            String testOne = new String("testOne");
            String testOneAnother = "testOne";
            System.out.println(testOne == testOneAnother); //false -> 上面說了testOne和testOneAnother是兩個物件,所以很容易得出結果
        }
    }

示例二


    public class TestString {

        public static void main(String[] args) {
            String s1 = "abc";
            String s2 = "a";
            String s3 = "bc";
            String s4 = s2 + s3; // StringBuilder.append(a).append(bc) -> StringBuilder.toString() -> new String("abc")
            System.out.println(s1 == s4); //false, s1指向了常量池中的字串abc,s4相當於生成了新的物件
        }
    }


    /**
     * 這裡就不貼常量池的資訊了,反正我們能確定的是在編譯時期字串已經載入到常量池中了,所以常量池中應該存在abc、a、bc字串
     * 貼出程式碼的執行邏輯:
     *  public static void main(java.lang.String[]);
     *      Code:
     *          0: ldc           #2                  // String abc   ->  ldc:從常量池中載入指定項的引用到棧
     *          2: astore_1                                          ->  abstor_1:將引用賦值給第1個區域性變數, 即 s1 = "abc"
     *          3: ldc           #3                  // String a
     *          5: astore_2                                          ->  s2 = "a"
     *          6: ldc           #4                  // String bc
     *          8: astore_3                                          ->  s3 = "bc"
     *          9: new           #5                  // class java/lang/StringBuilder    -> 建立StringBuilder物件
     *          12: dup    ->  複製StringBuilder物件的引用並壓入棧中
     *          13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V    -> 初始化StringBuilder物件
     *          16: aload_2                            -> 載入第二個區域性變數的值
     *          17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;  -> 呼叫常量池中指定項的方法,即呼叫StringBuilder物件中的append方法,並傳入序號 *                                                                                                                                           16中的引用,所以最終是StringBuilder#append(a)
     *          20: aload_3
     *          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;      -> StringBuilder#toString, 可以看下該類的toString方法,生成了一個新的物件,該物件中的字串不會再*                                                                                                                        常量池中生成
     *          27: astore        4                    -> 將序號24中的引用賦值給第4個區域性變數 s4 = StringBuilder.toString = new String()
     *          29: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;   -> 下面的就討論了,執行的是System.out.println程式碼片段了
     *          32: aload_1
     *          33: aload         4
     *          35: if_acmpne     42
     *          38: iconst_1
     *          39: goto          43
     *          42: iconst_0
     *          43: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
     *          46: return
     *
     * 
     * 從上面的分析中我們得出結論:s1指向常量池中的字串物件abc,而從序號16-27我們知道生成了新的物件,相當於是執行了new String("abc"),而這不就又回到了示例一了嗎,結果自然是false
     */

示例三


    public class TestString {

        public static void main(String[] args) {
            String s1 = "abc";
            final String s2 = "a";
            final String s3 = "bc";
            String s4 = s2 + s3; //由於s2、s3被final修飾了,故而直接替換變數的值,最後s4 = "abc",直接使用了常量池中的字串物件abc
            System.out.println(s1 == s4); //true
        }
    }

    /**
     * 同樣貼出程式碼的執行邏輯:
     *  public static void main(java.lang.String[]);
     *      Code:
     *          0: ldc           #2                  // String abc  -> ldc:從常量池中載入指定項的引用到棧
     *          2: astore_1                                         -> s1 = "abc"
     *          3: ldc           #3                  // String a
     *          5: astore_2                                         -> s2 = "a" 
     *          6: ldc           #4                  // String bc
     *          8: astore_3                                         -> s3 = "bc"
     *          9: ldc           #2                  // String abc
     *          11: astore        4                                 -> s4 = "abc"  示例三與示例二的區別在於加了final修飾,被final修飾的變數會在編譯階段直接替換成對應的值,即"a" + "bc",而這個在示例四中我們也會分析到,是直接採用
     *                                                                             字串合併,而合併後的字串abc在常量池中已經存在了,故直接使用
     *          13: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
     *          16: aload_1
     *          17: aload         4
     *          19: if_acmpne     26
     *          22: iconst_1
     *          23: goto          27
     *          26: iconst_0
     *          27: invokevirtual #6                  // Method java/io/PrintStream.println:(Z)V
     *          30: return
     * 從上面的分析中我們得出結論:s1指向了常量池中的字串物件abc,s4也是指向了常量池中的字串物件abc
     */


示例四


    public class TestString {

        public static void main(String[] args) {
            String s1 = "abc";
            String s2 = "a" + "bc"; //s1、s2指向同一個字串
            String s3 = "test" + "One"; //說明常量池是直接儲存合併後的字串
            System.out.println(s1 == s2); //true
        }
    }

    /**
     * 這裡多貼出常量池的資訊,為了說明s3的行為
     * Constant pool:
     * ...
     * #30 = Utf8               abc
     * #31 = Utf8               testOne
     * ...
     * 省略了一部分資訊,說明常量池是直接儲存合併後的字串,而並分開儲存,所以常量池中只會有"testOne",並沒有"test"或"One"
     *
     *  public static void main(java.lang.String[]);
     *       Code:
     *          0: ldc           #2                  // String abc
     *          2: astore_1                            -> s1 = "abc" 指向常量池中#2的引用
     *          3: ldc           #2                  // String abc
     *          5: astore_2                            -> s2 = "abc" 從常量池中#2的引用,可以看到引用的字串物件是同一個
     *          6: ldc           #3                  // String testOne
     *          8: astore_3
     *          9: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
     *          12: aload_1
     *          13: aload_2
     *          14: if_acmpne     21
     *          17: iconst_1
     *          18: goto          22
     *          21: iconst_0
     *          22: invokevirtual #5                  // Method java/io/PrintStream.println:(Z)V
     *          25: return
     * 從上面的分析中我們得出結論:s1、s2指向了常量池中的同一個字串物件
     */

示例五


    public class TestString {

        public static void main(String[] args) {
            String s1 = "abc";
            String s2 = "a";
            String s3 = s2 + "bc"; // StringBuilder.append(a).append(bc) -> StringBuilder.toString() -> new String("abc")
            System.out.println(s1 == s3);//false
        }
    }
    /**
     * public static void main(java.lang.String[]);
     *       Code:
     *          0: ldc           #2                  // String abc
     *          2: astore_1                             -> s1 = "abc"
     *          3: ldc           #3                  // String a
     *          5: astore_2                             -> s2 = "a"
     *          6: new           #4                  // class java/lang/StringBuilder   -> 建立StringBuilder物件
     *          9: dup           ->  複製StringBuilder物件的引用並壓入棧中
     *          10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V   -> 初始化StringBuilder
     *          13: aload_2                             -> 載入第二個區域性變數的值
     *          14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;   -> StringBuilder.append("a")
     *          17: ldc           #7                  // String bc
     *          19: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;   -> StringBuilder.append("bc")
     *          22: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;    -> StringBuilder.toString
     *          25: astore_3                           -> s3 = "abc"
     *          26: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
     *          29: aload_1
     *          30: aload_3
     *          31: if_acmpne     38
     *          34: iconst_1
     *          35: goto          39
     *          38: iconst_0
     *          39: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
     *          42: return
     * 從上面的分析中我們得出結論:s1指向常量池中的字串物件abc,而從序號9-25我們知道生成了新的物件,相當於是執行了new String("abc"),結果自然是false
     */


示例六

先來看下String#intern方法作了什麼動作,還是採用此分析方式。


    public class TestString {

        public static void main(String[] args) {
            String testOne = new String("a") + new String("bc"); //常量池中儲存字串a、bc,最終testOne指向堆記憶體中的物件,而該物件對應的字串是不會在常量池中存在
            String testOneAnother = testOne.intern(); //先去常量池中查詢是否已經存在該字串,如果存在,則返回常量池中的引用,若不存在則不會將該物件的字串拷貝到常量池中,而是在常量池中持有對該物件的引用
                                                      //這裡的引用沒辦法從該方式得出,可能需要看下native的方法,反正我是看了別人的分析,雖然我是知道原理但還是忍不住看了以下底層實現
            System.out.println(testOne == testOneAnther) //true
        }
    }
    /**
     * Constant pool:
     *  ...
     * #31 = Utf8               a
     *  ...
     * #34 = Utf8               bc
     */


示例七


    public class TestString {

        public static void main(String[] args) {
            String s1 = new String("a") + new String("bc");
            s1.intern(); //執行方法前我們知道常量池中並未有abc字串,執行該方法後,常量池中已經存在指向s1物件的引用,即"abc"字串的引用
            String s2 = "abc"; // 常量池中已經存在"abc"字串的引用,即為s1物件的引用
            System.out.println(s1 == s2);//true
        }
    }
    /**
     * public static void main(java.lang.String[]);
     *      Code:
     *          0: new           #2                  // class java/lang/StringBuilder    -> 建立StringBuilder物件
     *          3: dup                                -> 複製StringBuilder物件的引用並壓入棧中
     *          4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V  -> 初始化StringBuilder
     *          7: new           #4                  // class java/lang/String    -> 建立字串物件 new String()
     *          10: dup                               -> 複製String物件的引用並壓入棧中
     *          11: ldc           #5                  // String a
     *          13: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V    -> new String("a")
     *          16: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; -> StringBuilder.append()
     *          19: new           #4                  // class java/lang/String    ->  建立字串物件 new String()
     *          22: dup                               -> 複製String物件的引用並壓入棧中
     *          23: ldc           #8                  // String bc
     *          25: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V   -> new String("bc")
     *          28: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; -> StringBuilder.append()
     *          31: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;  -> StringBuilder.toString()
     *          34: astore_1                          -> s1 = "abc", 由StringBuilder#toString建立的字串物件
     *          35: aload_1                           -> 載入第一個區域性變數的值
     *          36: invokevirtual #10                 // Method java/lang/String.intern:()Ljava/lang/String;  -> s1.intern()
     *          39: pop                               -> pop:移除棧頂的值
     *          40: ldc           #11                 // String abc
     *          42: astore_2                          -> s2 = "abc"
     *          43: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
     *          46: aload_1
     *          47: aload_2
     *          48: if_acmpne     55
     *          51: iconst_1
     *          52: goto          56
     *          55: iconst_0
     *          56: invokevirtual #13                 // Method java/io/PrintStream.println:(Z)V
     *          59: return
     */


示例八


    public class TestString {

        public static void main(String[] args) {
            String s1 = new String("abc"); //生成兩個物件,堆記憶體一個,常量池一個
            s1.intern(); //常量池中已經存在該字串物件,故而直接返回
            String s2 = "abc"; //指向常量池的字串物件
            System.out.println(s1 == s2);//false  -> s1指向堆記憶體中的物件,s2指向常量池的物件
        }
    }

總結

  • 在編譯階段,字串已經被儲存與常量池中。

  • new String("abc"):一共有兩個不同的物件,一個在堆記憶體、一個在常量池。

  • s2 + s3拼接(s2、s3未被final修飾):底層建立StringBuilder物件,通過append拼接起來,最終呼叫toString生成一個新的物件。

  • "a" + "bc"直接拼接:直接將拼接後的字串儲存於常量池中。

  • s2 + "bc"拼接(s2未被final修飾):底層建立StringBuilder物件,通過append拼接起來,最終呼叫toString生成一個新的物件。

  • s.intern:若常量池中存在字串,則直接返回引用,若不存在,則在常量池中生成指向該字串物件的引用,後續若有宣告此字串,會返回指向該字串物件的引用,也就是同一個引用(參考示例七、八)。

重點

new String與String的區別 (s1 + s2)與("a" + "bc")的區別 intern