天天看點

為什麼阿裡巴巴Java開發手冊中不建議在循環體中使用+進行字元串拼接?為什麼 StringBuilder 比 + 快這麼多?使用 + 拼接字元串使用 StringBuilder 拼接字元串總結

之前在閱讀《阿裡巴巴Java開發手冊》時,發現有一條是關于循環體中字元串拼接的建議,具體内容如下:

為什麼阿裡巴巴Java開發手冊中不建議在循環體中使用+進行字元串拼接?為什麼 StringBuilder 比 + 快這麼多?使用 + 拼接字元串使用 StringBuilder 拼接字元串總結
那麼我們首先來用例子來看看在循環體中用 + 或者用 StringBuilder 進行字元串拼接的效率如何吧(JDK版本為 jdk1.8.0_201)。

public class StringConcatDemo {
    public static void main(String[] args) {
        long s1 = System.currentTimeMillis();
        new StringConcatDemo().addMethod();
        System.out.println("使用 + 拼接:" + (System.currentTimeMillis() - s1));

        s1 = System.currentTimeMillis();
        new StringConcatDemo().stringBuilderMethod();
        System.out.println("使用 StringBuilder 拼接:" + (System.currentTimeMillis() - s1));
    }

    public String addMethod() {
        String result = "";
        for (int i = 0; i < 100000; i++) {
            result += (i + "wupx");
        }
        return result;
    }

    public String stringBuilderMethod() {
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < 100000; i++) {
            result.append(i).append("wupx");
        }
        return result.toString();
    }
}           

執行結果如下:

使用 + 拼接:29282
使用 StringBuilder 拼接:4           

為什麼這兩種方法的時間會差這麼多呢?接下來讓我們一起進一步研究。

為什麼 StringBuilder 比 + 快這麼多?

從位元組碼層面來看下,為什麼循環體中字元串拼接 StringBuilder 比 + 快這麼多?

使用 javac StringConcatDemo.java 指令編譯源檔案,使用 javap -c StringConcatDemo 指令檢視位元組碼檔案的内容。

其中 addMethod() 方法的位元組碼如下:

public java.lang.String addMethod();
Code:
   0: ldc           #16                 // String
   2: astore_1
   3: iconst_0
   4: istore_2
   5: iload_2
   6: ldc           #17                 // int 100000
   8: if_icmpge     41
  11: new           #7                  // class java/lang/StringBuilder
  14: dup
  15: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V
  18: aload_1
  19: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  22: iload_2
  23: invokevirtual #18                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  26: ldc           #19                 // String wupx
  28: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  31: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  34: astore_1
  35: iinc          2, 1
  38: goto          5
  41: aload_1
  42: areturn           

可以看出,第 8 行到第 38 行構成了一個循環體:在第 8 行的時候做條件判斷,如果不滿足循環條件,則跳轉到 41 行。編譯器做了一定程度的優化,在 11 行 new 了一個 StringBuilder 對象,然後再 19 行、23 行、28 行進行了三次 append() 方法的調用,不過每次循環都會重新 new 一個 StringBuilder 對象。

再來看 stringBuilderMethod() 方法的位元組碼:

public java.lang.String stringBuilderMethod();
Code:
   0: new           #7                  // class java/lang/StringBuilder
   3: dup
   4: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V
   7: astore_1
   8: iconst_0
   9: istore_2
  10: iload_2
  11: ldc           #17                 // int 100000
  13: if_icmpge     33
  16: aload_1
  17: iload_2
  18: invokevirtual #18                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  21: ldc           #19                 // String wupx
  23: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  26: pop
  27: iinc          2, 1
  30: goto          10
  33: aload_1
  34: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  37: areturn           

13 行到 30 行構成了循環體,可以看出,在第4行(循環體外)就建構好了 StringBuilder 對象,然後再循環體内隻進行 append() 方法的調用。

由此可以看出,在 for 循環中,使用 + 進行字元串拼接,每次都是 new 了一個 StringBuilder,然後再把 String 轉成 StringBuilder,再進行 append,而頻繁的建立對象不僅要耗費很多時間,還會造成記憶體資源的浪費。這就從位元組碼層面解釋了為什麼不建議在循環體内使用 + 去進行字元串的拼接。

接下來再來讓我們看下使用 + 或者 StringBuilder 拼接字元串的原理吧。

使用 + 拼接字元串

在 Java 開發中,最簡單常用的字元串拼接方法就是直接使用 + 來完成:

String boy = "wupx";
String girl = "huxy";
String love = boy + girl;           

反編譯後的内容如下:(使用的反編譯工具為 jad)

String boy = "wupx";
String girl = "huxy";
String love = (new StringBuilder()).append(boy).append(girl).toString();           

通過檢視反編譯以後的代碼,可以發現,在字元串常量在拼接過程中,是将 String 轉成了 StringBuilder 後,使用其 append() 方法進行處理的。

那麼也就是說,Java中的 + 對字元串的拼接,其實作原理是使用 StringBuilder 的 append() 來實作的,使用 + 拼接字元串,其實隻是 Java 提供的一個文法糖。

使用 StringBuilder 拼接字元串

StringBuilder 的 append 方法就是第二個常用的字元串拼接姿勢了。

和 String 類類似,StringBuilder 類也封裝了一個字元數組,定義如下:

char[] value;           

與 String 不同的是,它并不是 final 的,是以是可以修改的。另外,與 String 不同,字元數組中不一定所有位置都已經被使用,它有一個執行個體變量,表示數組中已經使用的字元個數,定義如下:

int count;           

其 append() 方法源碼如下:

public StringBuilder append(String str) {
   super.append(str);
   return this;
}           

該類繼承了 AbstractStringBuilder 類,看下其 append() 方法:

public AbstractStringBuilder append(String str) {
    if (str == null)
        return appendNull();
    int len = str.length();
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}           

首先判斷拼接的字元串 str 是不是 null,如果是,調用 appendNull() 方法進行處理,appendNull() 方法的源碼如下:

private AbstractStringBuilder appendNull() {
    int c = count;
    ensureCapacityInternal(c + 4);
    final char[] value = this.value;
    value[c++] = 'n';
    value[c++] = 'u';
    value[c++] = 'l';
    value[c++] = 'l';
    count = c;
    return this;
}           

如果字元串 str 不為 null,則判斷拼接後的字元數組長度是否超過目前數組長度,如果超過,則調用 Arrays.copyOf() 方法進行擴容并複制,ensureCapacityInternal() 方法的源碼如下:

private void ensureCapacityInternal(int minimumCapacity) {
    if (minimumCapacity - value.length > 0) {
        value = Arrays.copyOf(value,
                newCapacity(minimumCapacity));
    }
}           

最後,将拼接的字元串 str 複制到目标數組 value 中。

str.getChars(0, len, value, count);           

總結

本文針對《阿裡巴巴Java開發手冊》中的循環體中拼接字元串建議出發,從位元組碼層面,來解釋為什麼 StringBuilder 比 + 快,還分别介紹了字元串拼接中 + 和 StringBuilder 的原理,是以在循環體拼接字元串時,應該使用 StringBuilder 的 append() 去完成拼接。

參考

《Java開發手冊》華山版

《Effective Java(第二版)》

《Java程式設計思想》