StringBuilder 的 append() 方法
“循環體内,拼接字元串最好使用 StringBuilder 的 append() 方法,而不是 + 号操作符”
這句話,我們很熟悉,那你知道是為什麼嗎?
+
号操作符其實被 Java 在編譯的時候重新解釋了,換一種說法就是,
+
号操作符是一種文法糖,讓字元串的拼接變得更簡便了。
class Demo {
public static void main(String[] args) {
String chenmo = "沉默";
String wanger = "王二";
System.out.println(chenmo + wanger);
}
}
在 Java 8 的環境下,使用 javap -c Demo.class 反編譯位元組碼後,可以看到以下内容:
Compiled from "Demo.java"
class Demo {
Demo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String 沉默
2: astore_1
3: ldc #3 // String 王二
5: astore_2
6: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: return
}
看第9行,這裡有一個 new 關鍵字,并且 class 類型為 java/lang/StringBuilder。
“這意味着**建立了一個 StringBuilder 的對象**。”
然後看标号為 17 的這行,是一個 invokevirtual 指令,用于調用對象的方法,
也就是 StringBuilder 對象的 append() 方法。
也就意味着把 chenmo 這個字元串添加到 StringBuilder 對象中了。
再往下看,标号為 21 的這行,又調用了一次 append() 方法,
意味着把 wanger 這個字元串添加到 StringBuilder 對象中了。
換成 Java 代碼來表示的話,大概是這個樣子:
class Demo {
public static void main(String[] args) {
String chenmo = "沉默";
String wanger = "王二";
System.out.println((new StringBuilder(String.valueOf(chenmo))).append(wanger).toString());
}
}
原來編譯的時候把“
+
”号操作符替換成了
StringBuilder
的
append()
方法啊。
是的,不過到了 Java 9,情況發生了一些改變,同樣的代碼,位元組碼指令完全不同了。
同樣的代碼,在 Java 11 的環境下,位元組碼指令是這樣的:
Compiled from "Demo.java"
public class com.itwanger.thirtyseven.Demo {
public com.itwanger.thirtyseven.Demo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String
2: astore_1
3: iconst_0
4: istore_2
5: iload_2
6: bipush 10
8: if_icmpge 41
11: new #3 // class java/lang/String
14: dup
15: ldc #4 // String 沉默
17: invokespecial #5 // Method java/lang/String."<init>":(Ljava/lang/String;)V
20: astore_3
21: ldc #6 // String 王二
23: astore 4
25: aload_1
26: aload_3
27: aload 4
29: invokedynamic #7, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
34: astore_1
35: iinc 2, 1
38: goto 5
41: return
}
看标号為 29 的這行,位元組碼指令為 invokedynamic,
該指令允許由應用級的代碼來決定方法解析,所謂的應用級的代碼其實是一個方法——被稱為引導方法(Bootstrap Method),簡稱 BSM,
BSM 會傳回一個 CallSite(調用點) 對象,這個對象就和 invokedynamic 指令連結在一起。
以後再執行這條 invokedynamic 指令時就不會建立新的 CallSite 對象。
CallSite 其實就是一個 MethodHandle(方法句柄)的 holder,
指向一個調用點真正執行的方法——此時就是 StringConcatFactory.makeConcatWithConstants() 方法。
好吧,總之就是 Java 9 以後,JDK 用了另外一種方法來
動态解釋 + 号操作符
,具體的實作方式在位元組碼指令層面已經看不到了
循環體内,拼接字元串最好使用 StringBuilder 的 append() 方法,而不是 + 号操作符。原因就在于循環體内如果用 + 号操作符的話,就會産生大量的 StringBuilder 對象,不僅占用了更多的記憶體空間,還會讓 Java 虛拟機不停的進行垃圾回收,進而降低了程式的性能。
更好的寫法就是在循環的外部建立一個 StringBuilder 對象,然後使用 append() 方法将循環體内的字元串添加進來:
class Demo {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
for (int i = 1; i < 10; i++) {
String chenmo = "沉默";
String wanger = "王二";
sb.append(chenmo);
sb.append(wanger);
}
System.out.println(sb);
}
}
來做個小測試。
第一個,for 循環中使用”+”号操作符。
String result = "";
for (int i = 0; i < 100000; i++) {
result += "六六六";
}
第二個,for 循環外部建立 StringBuilder,循環體内使用 append() 方法。
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
sb.append("六六六");
}
package com.study;
/**
* @Description 測試字元串拼接+,StringBuilder
* @Classname JoinStringsDemo
* @Date 2021/8/22 16:11
* @Created by 折騰的小飛
*/
public class JoinStringsDemo {
public static void main(String[] args) {
new Thread(()->{
long startTime=System.currentTimeMillis();
String result = "";
for (int i = 0; i < 100000; i++) {
result += "六六六";
}
long endTime=System.currentTimeMillis();
System.out.println(endTime-startTime);
}).start();
new Thread(()->{
long startTime=System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
sb.append("六六六");
}
long endTime=System.currentTimeMillis();
System.out.println(endTime-startTime);
}).start();
}
}
使用+拼接字元串執行時間是 4892 毫秒,使用StringBuffer隻用了不到 11毫秒,差距也太大了吧!
來看一下 StringBuilder 類的 append() 方法的源碼吧!
public StringBuilder append(String str) {
super.append(str);
return this;
}
這 3 行代碼其實沒啥看的。我們來看
父類 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;
}
1)判斷拼接的字元串是不是 null,如果是,當做字元串“null”來處理。
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;
}
2)擷取字元串的長度。
3)
ensureCapacityInternal()
方法的源碼如下:
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
由于字元串内部是用數組實作的,是以需要先判斷拼接後的字元數組長度是否超過目前數組的長度,如果超過,先對數組進行擴容,然後把原有的值複制到新的數組中。
4)将拼接的字元串 str 複制到目标數組 value 中。
5)更新數組的長度 count。
說到 StringBuilder 就必須得提一嘴 StringBuffer,兩者就像是孿生雙胞胎,該有的都有,隻不過大哥 StringBuffer 因為多呼吸兩口新鮮空氣,是以是線程安全的。”我說,“它裡面的方法基本上都加了 synchronized 關鍵字來做同步。
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
除了可以使用 + 号操作符,StringBuilder 和 StringBuilder 的 append() 方法,還有其他的字元串拼接方法嗎?
String 類的 concat() 方法
String chenmo = "沉默";
String wanger = "王二";
System.out.println(chenmo.concat(wanger));
可以來看一下
concat()
方法的源碼。
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
1)如果拼接的字元串的長度為 0,那麼傳回拼接前的字元串。
2)将原字元串的字元數組 value 複制到變量 buf 數組中。
3)把拼接的字元串 str 複制到字元數組 buf 中,并傳回新的字元串對象。
和 + 号操作符相比,
concat()
方法在遇到字元串為 null 的時候,會抛出
NullPointerException
,而“+”号操作符會把 null 當做是“null”字元串來處理。
如果拼接的字元串是一個空字元串(""),那麼 concat 的效率要更高一點,
畢竟不需要 new StringBuilder 對象。
如果拼接的字元串非常多,concat() 的效率就會下降,
因為建立的字元串對象越來越多。
String 類有一個靜态方法 join()
String chenmo = "沉默";
String wanger = "王二";
String cmower = String.join("", chenmo, wanger);
System.out.println(cmower);
第一個參數為字元串連接配接符
輸出結果為:
王二-太特麼-有趣了
。
來看一下 join 方法的源碼:
public static String join(CharSequence delimiter, CharSequence... elements) {
Objects.requireNonNull(delimiter);
Objects.requireNonNull(elements);
// Number of elements not likely worth Arrays.stream overhead.
StringJoiner joiner = new StringJoiner(delimiter);
for (CharSequence cs: elements) {
joiner.add(cs);
}
return joiner.toString();
}
裡面建立了一個叫
StringJoiner
的對象,然後通過 for-each 循環把可變參數添加了進來,最後調用 toString() 方法傳回 String。
org.apache.commons.lang3.StringUtils的join() 方法
實際的工作中,
org.apache.commons.lang3.StringUtils
的
join()
方法也經常用來進行字元串拼接。
String chenmo = "沉默";
String wanger = "王二";
StringUtils.join(chenmo, wanger);
該方法不用擔心 NullPointerException。
StringUtils.join(null) = null
StringUtils.join([]) = ""
StringUtils.join([null]) = ""
StringUtils.join(["a", "b", "c"]) = "abc"
StringUtils.join([null, "", "a"]) = "a"
來看一下源碼:
public static String join(final Object[] array, String separator, final int startIndex, final int endIndex) {
if (array == null) {
return null;
}
if (separator == null) {
separator = EMPTY;
}
final StringBuilder buf = new StringBuilder(noOfItems * 16);
for (int i = startIndex; i < endIndex; i++) {
if (i > startIndex) {
buf.append(separator);
}
if (array[i] != null) {
buf.append(array[i]);
}
}
return buf.toString();
}
内部使用的仍然是
StringBuilder
。