## 如何拼接字符串

“哥，你让我看的《Java 开发手册》上有这么一段内容：循环体内，拼接字符串最好使用 StringBuilder 的 `append()` 方法，而不是 + 号操作符。这是为什么呀？”三妹疑惑地问。

“好的，三妹，哥来慢慢给你讲。”我回答。

三妹能在学习的过程中不断地发现问题，让我感到非常的开心。其实很多时候，我们不应该只是把知识点记在心里，还应该问一问自己，到底是为什么，只有迈出去这一步，才能真正的成长起来。

“+ 号操作符其实被 Java 在编译的时候重新解释了，换一种说法就是，+ 号操作符是一种语法糖，让字符串的拼接变得更简便了。”一边给三妹解释，我一边在 Intellij IDEA 中敲出了下面这段代码。

```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
}
```

“你看，三妹，这里有一个 new 关键字，并且 class 类型为 `java/lang/StringBuilder`。”我指着标号为 9 的那行对三妹说，“这意味着新建了一个 StringBuilder 的对象。”

“然后看标号为 17 的这行，是一个 invokevirtual 指令，用于调用对象的方法，也就是 StringBuilder 对象的 `append()` 方法。”

“也就意味着把 chenmo 这个字符串添加到 StringBuilder 对象中了。”

“再往下看，标号为 21 的这行，又调用了一次 `append()` 方法，意味着把 wanger 这个字符串添加到 StringBuilder 对象中了。”

换成 Java 代码来表示的话，大概是这个样子：

```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()` 方法将循环体内的字符串添加进来：

```java
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 循环中使用”+”号操作符。

```java
String result = "";
for (int i = 0; i < 100000; i++) {
    result += "六六六";
}
```

第二个，for 循环外部新建 StringBuilder，循环体内使用 `append()` 方法。

```java
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
    sb.append("六六六");
}
```

“这两个小测试分别会耗时多长时间呢？三妹你来运行下。”

“哇，第一个小测试的执行时间是 6212 毫秒，第二个只用了不到 1 毫秒，差距也太大了吧！”三妹说。

“是的，这下明白了原因吧？”我说。

“是的，哥，原来如此。”

“好了，三妹，来看一下 StringBuilder 类的 `append()` 方法的源码吧！”

```java
public StringBuilder append(String str) {
    super.append(str);
    return this;
}
```

这 3 行代码其实没啥看的。我们来看父类 AbstractStringBuilder 的 `append()` 方法：

```java
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()` 方法的源码如下：

```java
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()` 方法的源码如下：

```java
private void ensureCapacityInternal(int minimumCapacity) {
    // overflow-conscious code
    if (minimumCapacity - value.length > 0) {
        value = Arrays.copyOf(value,
                newCapacity(minimumCapacity));
    }
}
```

由于字符串内部是用数组实现的，所以需要先判断拼接后的字符数组长度是否超过当前数组的长度，如果超过，先对数组进行扩容，然后把原有的值复制到新的数组中。

 4）将拼接的字符串 str 复制到目标数组 value 中。

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

5）更新数组的长度 count。

“说到 StringBuilder 就必须得提一嘴 StringBuffer，两者就像是孪生双胞胎，该有的都有，只不过大哥 StringBuffer 因为多呼吸两口新鲜空气，所以是线程安全的。”我说，“它里面的方法基本上都加了 synchronized 关键字来做同步。”

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

“除了可以使用 + 号操作符，StringBuilder 和 StringBuilder 的 `append()` 方法，还有其他的字符串拼接方法吗？”三妹问。

“有啊，比如说 String 类的 `concat()` 方法，有点像 StringBuilder 类的 `append()` 方法。”

```java
String chenmo = "沉默";
String wanger = "王二";
System.out.println(chenmo.concat(wanger));
```

可以来看一下 `concat()` 方法的源码。

```java
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()`，可以这样来使用。

```java
String chenmo = "沉默";
String wanger = "王二";
String cmower = String.join("", chenmo, wanger);
System.out.println(cmower);
```

第一个参数为字符串连接符，比如说：

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

输出结果为：`王二-太特么-有趣了`。

来看一下 join 方法的源码：

```java
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()` 方法也经常用来进行字符串拼接。”

```java
String chenmo = "沉默";
String wanger = "王二";
StringUtils.join(chenmo, wanger);
```

该方法不用担心 NullPointerException。

```java
StringUtils.join(null)            = null
StringUtils.join([])              = ""
StringUtils.join([null])          = ""
StringUtils.join(["a", "b", "c"]) = "abc"
StringUtils.join([null, "", "a"]) = "a"
```

来看一下源码：

```java
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 以后，对 + 号操作符的解释和之前发生了变化，字节码指令已经不同了，等后面你学了字节码指令后我们再详细地讲一次。”我说。

“嗯，哥，你休息吧，我把这些例子再重新跑一遍。”三妹说。