之前在閱讀《阿裡巴巴Java開發手冊》時,發現有一條是關於迴圈體中字元串拼接的建議,具體內容如下: 那麼我們首先來用例子來看看在迴圈體中用 + 或者用 StringBuilder 進行字元串拼接的效率如何吧(JDK版本為 jdk1.8.0_201)。 可以看出,第 8 行到第 38 行構成了一個迴圈 ...
之前在閱讀《阿裡巴巴Java開發手冊》時,發現有一條是關於迴圈體中字元串拼接的建議,具體內容如下:
那麼我們首先來用例子來看看在迴圈體中用 + 或者用 StringBuilder 進行字元串拼接的效率如何吧(JDK版本為 jdk1.8.0_201)。
package com.wupx.demo;
/**
* @author wupx
* @date 2019/10/23
*/
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 + "武培軒");
}
return result;
}
public String stringBuilderMethod() {
StringBuilder result = new StringBuilder();
for (int i = 0; i < 100000; i++) {
result.append(i).append("武培軒");
}
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 = "huyx";
String love = boy + girl;
反編譯後的內容如下:(使用的反編譯工具為 jad)
String boy = "wupx";
String girl = "huyx";
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() 去完成拼接。