之前在閱讀《阿里巴巴Java開(kāi)發(fā)手冊(cè)》時(shí),發(fā)現(xiàn)有一條是關(guān)于循環(huán)體中字符串拼接的建議,具體內(nèi)容如下:
那么我們首先來(lái)用例子來(lái)看看在循環(huán)體中用 + 或者用 StringBuilder 進(jìn)行字符串拼接的效率如何吧(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(); } }
執(zhí)行結(jié)果如下:
使用 + 拼接:29282 使用 StringBuilder 拼接:4
為什么這兩種方法的時(shí)間會(huì)差這么多呢?接下來(lái)讓我們一起進(jìn)一步研究。
為什么 StringBuilder 比 + 快這么多?
從字節(jié)碼層面來(lái)看下,為什么循環(huán)體中字符串拼接 StringBuilder 比 + 快這么多?
使用 javac StringConcatDemo.java 命令編譯源文件,使用 javap -c StringConcatDemo
命令查看字節(jié)碼文件的內(nèi)容。
其中 addMethod() 方法的字節(jié)碼如下:
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 行構(gòu)成了一個(gè)循環(huán)體:在第 8 行的時(shí)候做條件判斷,如果不滿足循環(huán)條件,則跳轉(zhuǎn)到 41 行。編譯器做了一定程度的優(yōu)化,在 11
行 new 了一個(gè) StringBuilder 對(duì)象,然后再 19 行、23 行、28 行進(jìn)行了三次 append() 方法的調(diào)用,不過(guò)每次循環(huán)都會(huì)重新
new 一個(gè) StringBuilder 對(duì)象。
再來(lái)看 stringBuilderMethod() 方法的字節(jié)碼:
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 行構(gòu)成了循環(huán)體,可以看出,在第4行(循環(huán)體外)就構(gòu)建好了 StringBuilder 對(duì)象,然后再循環(huán)體內(nèi)只進(jìn)行 append()
方法的調(diào)用。
由此可以看出,在 for 循環(huán)中,使用 + 進(jìn)行字符串拼接,每次都是 new 了一個(gè) StringBuilder,然后再把 String 轉(zhuǎn)成
StringBuilder,再進(jìn)行 append,而頻繁的新建對(duì)象不僅要耗費(fèi)很多時(shí)間,還會(huì)造成內(nèi)存資源的浪費(fèi)。這就從字節(jié)碼層面解釋了為什么不建議在循環(huán)體內(nèi)使用
+ 去進(jìn)行字符串的拼接。
接下來(lái)再來(lái)讓我們看下使用 + 或者 StringBuilder 拼接字符串的原理吧。
使用 + 拼接字符串
在 Java 開(kāi)發(fā)中,最簡(jiǎn)單常用的字符串拼接方法就是直接使用 + 來(lái)完成:
String boy = "wupx"; String girl = "huyx"; String love = boy + girl;
反編譯后的內(nèi)容如下:(使用的反編譯工具為 jad)
String boy = "wupx"; String girl = "huyx"; String love = (new
StringBuilder()).append(boy).append(girl).toString();
通過(guò)查看反編譯以后的代碼,可以發(fā)現(xiàn),在字符串常量在拼接過(guò)程中,是將 String 轉(zhuǎn)成了 StringBuilder 后,使用其 append()
方法進(jìn)行處理的。
那么也就是說(shuō),Java中的 + 對(duì)字符串的拼接,其實(shí)現(xiàn)原理是使用 StringBuilder 的 append() 來(lái)實(shí)現(xiàn)的,使用 + 拼接字符串,其實(shí)只是
Java 提供的一個(gè)語(yǔ)法糖。
使用 StringBuilder 拼接字符串
StringBuilder 的 append 方法就是第二個(gè)常用的字符串拼接姿勢(shì)了。
和 String 類類似,StringBuilder 類也封裝了一個(gè)字符數(shù)組,定義如下:
char[] value;
與 String 不同的是,它并不是 final 的,所以是可以修改的。另外,與 String
不同,字符數(shù)組中不一定所有位置都已經(jīng)被使用,它有一個(gè)實(shí)例變量,表示數(shù)組中已經(jīng)使用的字符個(gè)數(shù),定義如下:
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,如果是,調(diào)用 appendNull() 方法進(jìn)行處理,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,則判斷拼接后的字符數(shù)組長(zhǎng)度是否超過(guò)當(dāng)前數(shù)組長(zhǎng)度,如果超過(guò),則調(diào)用 Arrays.copyOf()
方法進(jìn)行擴(kuò)容并復(fù)制,ensureCapacityInternal() 方法的源碼如下:
private void ensureCapacityInternal(int minimumCapacity) { if (minimumCapacity
- value.length > 0) { value = Arrays.copyOf(value,
newCapacity(minimumCapacity)); } }
最后,將拼接的字符串 str 復(fù)制到目標(biāo)數(shù)組 value 中。
str.getChars(0, len, value, count);
總結(jié)
本文針對(duì)《阿里巴巴Java開(kāi)發(fā)手冊(cè)》中的循環(huán)體中拼接字符串建議出發(fā),從字節(jié)碼層面,來(lái)解釋為什么 StringBuilder 比 +
快,還分別介紹了字符串拼接中 + 和 StringBuilder 的原理,因此在循環(huán)體拼接字符串時(shí),應(yīng)該使用 StringBuilder 的 append()
去完成拼接。
熱門(mén)工具 換一換
