天天看點

教妹學 Java 第 37 講:字元串拼接

“哥,你讓我看的《Java 開發手冊》上有這麼一段内容:循環體内,拼接字元串最好使用 StringBuilder 的

append()

方法,而不是 + 号操作符。這是為什麼呀?”三妹疑惑地問。

“好的,三妹,哥來慢慢給你講。”我回答。

三妹能在學習的過程中不斷地發現問題,讓我感到非常的開心。其實很多時候,我們不應該隻是把知識點記在心裡,還應該問一問自己,到底是為什麼,隻有邁出去這一步,才能真正的成長起來。

“+ 号操作符其實被 Java 在編譯的時候重新解釋了,換一種說法就是,+ 号操作符是一種文法糖,讓字元串的拼接變得更簡便了。”一邊給三妹解釋,我一邊在 Intellij IDEA 中敲出了下面這段代碼。

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
}
           

複制

“你看,三妹,這裡有一個 new 關鍵字,并且 class 類型為

java/lang/StringBuilder

。”我指着标号為 9 的那行對三妹說,“這意味着建立了一個 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 用了另外一種方法來動态解釋 + 号操作符,具體的實作方式在位元組碼指令層面已經看不到了,是以我就以 Java 8 來繼續講解吧。”

“再回到《Java 開發手冊》上的那段内容:循環體内,拼接字元串最好使用 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("六六六");
}
           

複制

“這兩個小測試分别會耗時多長時間呢?三妹你來運作下。”

“哇,第一個小測試的執行時間是 6212 毫秒,第二個隻用了不到 1 毫秒,差距也太大了吧!”三妹說。

“是的,這下明白了原因吧?”我說。

“是的,哥,原來如此。”

“好了,三妹,來看一下 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 中。

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

複制

5)更新數組的長度 count。

“說到 StringBuilder 就必須得提一嘴 StringBuffer,兩者就像是孿生雙胞胎,該有的都有,隻不過大哥 StringBuffer 因為多呼吸兩口新鮮空氣,是以是線程安全的。”我說,“它裡面的方法基本上都加了 synchronized 關鍵字來做同步。”

public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}
           

複制

“除了可以使用 + 号操作符,StringBuilder 和 StringBuilder 的

append()

方法,還有其他的字元串拼接方法嗎?”三妹問。

“有啊,比如說 String 類的

concat()

方法,有點像 StringBuilder 類的

append()

方法。”

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);
           

複制

第一個參數為字元串連接配接符,比如說:

String message = String.join("-", "王二", "太特麼", "有趣了");
           

複制

輸出結果為:

王二-太特麼-有趣了

來看一下 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()

方法也經常用來進行字元串拼接。”

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。

“好了,三妹,關于字元串拼接的知識點我們就講到這吧。注意 Java 9 以後,對 + 号操作符的解釋和之前發生了變化,位元組碼指令已經不同了,等後面你學了位元組碼指令後我們再詳細地講一次。”我說。

“嗯,哥,你休息吧,我把這些例子再重新跑一遍。”三妹說。

---未完待續,期待下集---