以前在閱讀《阿里巴巴Java開發手冊》時,發現有一條是關於循環體中字符串拼接的建議,具體內容以下:java
那麼咱們首先來用例子來看看在循環體中用 + 或者用 StringBuilder 進行字符串拼接的效率如何吧(JDK版本爲 jdk1.8.0_201)。c++
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
爲何這兩種方法的時間會差這麼多呢?接下來讓咱們一塊兒進一步研究。app
從字節碼層面來看下,爲何循環體中字符串拼接 StringBuilder 比 + 快這麼多?工具
使用 javac StringConcatDemo.java 命令編譯源文件,使用 javap -c StringConcatDemo 命令查看字節碼文件的內容。優化
其中 addMethod() 方法的字節碼以下:ui
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 對象。this
再來看 stringBuilderMethod() 方法的字節碼:spa
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() 方法的調用。code
由此能夠看出,在 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 的 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() 去完成拼接。