1. 程式人生 > >從位元組碼看Java對字串拼接的優化

從位元組碼看Java對字串拼接的優化

Java中有個經典的問題,如下程式碼的輸出結果:

	String s1 = "a";
	String s2 = s1 + "b";
	String s3 = "a" + "b";
	System.out.println(s2 == "ab");
	System.out.println(s3 == "ab");

答案可以在這裡直接給出,輸出結果為 falsetrue,而給出的解釋是:對於字串的直接拼接,會在編譯階段編譯器將拼接結果計算出來,並將結果放在常量區中。

對於這個題目來說,s1是變數,導致s2需要在執行時才能拼接出來;而s3是a和b兩個常量拼接的結果,所以在編譯時就已經得出結果。這就導致了,s2指向的是執行時生成的ab,而s3指向的是編譯時存放在常量區的ab。

上面的這種解釋很籠統抽象,為了更深入地理解原理,需要檢視上面程式碼編譯後的位元組碼:

      stack=3, locals=4, args_size=1
         0: ldc           #16                 // String a
         2: astore_1
         3: new           #18                 // class java/lang/StringBuilder
         6: dup
         7: aload_1
         8: invokestatic  #20                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
        11: invokespecial #26                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
        14: ldc           #29                 // String b
        16: invokevirtual #31                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: invokevirtual #35                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        22: astore_2
        23: ldc           #39                 // String ab
        25: astore_3
        // 下面的程式碼是列印邏輯
        ......

在上面的位元組碼中,重點看3-22行,等價於:

  String s2=new StringBuilder(s1).append("b").toString();

而23-25行,說明s3是直接通過符號引用指向常量區中的String型別的ab。

由此我們得出結論:Java中帶有變數的字串拼接,實際上是通過StringBuilder的append方法完成的;而若干個常量的拼接,會在編譯階段編譯器"聰明"地完成,執行時直接引用常量區的欄位即可。很顯然,兩種方式對應著兩個不同的引用。

所以,考慮到編譯階段的優化,字串的拼接就可以無所忌憚地用+了嗎?答案是否定的!考慮下面兩段程式碼:

程式碼1:

    String a =
"test"; for (int i = 0; i < 10; i++) { a += i; }

程式碼2:

    StringBuilder builder = new StringBuilder("test");
    for (int i = 0; i < 10; i++) {
      builder.append(i);
    }
    String a = builder.toString();

上面的兩端程式碼從完成的功能角度看都是一致的:最終變數a的值都會變成test0123456789。但是檢視位元組碼後:

程式碼1位元組碼:

      stack=3, locals=2, args_size=0
         0: ldc           #70                 // String test
         2: astore_0
         3: iconst_0
         4: istore_1
         5: goto          30
         8: new           #18                 // class java/lang/StringBuilder
        11: dup
        12: aload_0
        13: invokestatic  #20                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
        16: invokespecial #26                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
        19: iload_1
        20: invokevirtual #72                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        23: invokevirtual #35                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        26: astore_0
        27: iinc          1, 1
        30: iload_1
        31: bipush        10
        33: if_icmplt     8
        36: return

程式碼2位元組碼:

      stack=3, locals=2, args_size=0
         0: new           #18                 // class java/lang/StringBuilder
         3: dup
         4: ldc           #70                 // String test
         6: invokespecial #26                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
         9: astore_0
        10: iconst_0
        11: istore_1
        12: goto          24
        15: aload_0
        16: iload_1
        17: invokevirtual #72                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        20: pop
        21: iinc          1, 1
        24: iload_1
        25: bipush        10
        27: if_icmplt     15
        30: aload_0
        31: invokevirtual #35                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        34: astore_1
        35: return

在程式碼1的位元組碼中,8-27行發現,迴圈體中每一次迴圈都會new出一個StringBuilder,對應的java程式碼為:

    String a = "test";
    for (int i = 0; i < 10; i++) {
      a = new StringBuilder(a).append(i).toString();
    }

在程式碼2的位元組碼中,15-27的迴圈體中,並不會每次一次迴圈new出一個StringBuilder。

顯然,程式碼2的執行效率要高於程式碼1的執行效率。究其緣由是,編譯器雖然會在編譯階段對字串的拼接做了優化,但是並沒有做到跨越程式碼塊級別的優化。所以在每一次迴圈的程式碼塊中,程式碼1都會new出一個StringBuilder。如果迴圈次數更多,會創建出更多的StringBuilder物件,最終有可能導致OOM等問題。