Skip to content

字符串拼接(原理/效率对比)

字符串拼接无处不在

"hello " + name + ", welcome to " + city —— 这是 Java 代码中最常见的操作之一。但 + 背后的代价,远比你想象的昂贵。

+ 拼接的底层原理

字节码告诉你真相

java
public class StringConcat {
    public static void main(String[] args) {
        String a = "hello";
        String b = "world";
        String c = a + b;
    }
}

javap -c 查看字节码:

java
public static void main(java.lang.String[]);
    Code:
       0: ldc           #2          // 加载 "hello"
       2: astore_1                  // 存到 a
       3: ldc           #3          // 加载 "world"
       5: astore_2                  // 存到 b
       6: new           #4          // 创建 StringBuilder
       9: dup                        // 复制引用
      10: invokespecial #5          // 调用 StringBuilder 构造器
      13: aload_1                  // 加载 a
      14: invokevirtual #6          // StringBuilder.append(a)
      17: aload_2                  // 加载 b
      18: invokevirtual #6          // StringBuilder.append(b)
      21: invokevirtual #7          // StringBuilder.toString()
      24: astore_3                  // 存到 c

真相a + b 实际上被 javac 编译器转换成了:

java
new StringBuilder()
    .append(a)
    .append(b)
    .toString();

+ 拼接的完整过程

a + b 编译后:
  new StringBuilder()    → 分配内存

  .append(a)             → StringBuilder 追加

  .append(b)             → StringBuilder 追加

  .toString()            → 生成新的 String 对象

StringBuilder.toString() 的实现:

java
public String toString() {
    // 创建了一个新的 String 对象,不共享 value 数组
    return new String(this, 0, count);
}

循环中的拼接灾难

java
public class LoopConcat {
    public static void main(String[] args) {
        // 这个循环会创建多少个 StringBuilder 和 String?
        String result = "";
        for (int i = 0; i < 100; i++) {
            result += i;  // 每次循环都 new StringBuilder + toString()
        }
        // 答案:100 个 StringBuilder + 100 个新的 String 对象
        // 效率极低!
    }
}

编译后的字节码(简化):

java
// 循环开始
String result = "";
for (int i = 0; i < 100; i++) {
    StringBuilder sb = new StringBuilder();  // ❌ 每次循环都 new!
    sb.append(result);                       // ❌ 每次都复制已有内容
    sb.append(i);
    result = sb.toString();                  // ❌ 每次都 new String
}

正确的拼接方式

方式一:StringBuilder(显式)

java
public class StringBuilderConcat {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 100; i++) {
            sb.append(i);
        }
        String result = sb.toString();
    }
}

字节码:

java
new StringBuilder()
    iconst_0
    istore_2        // i = 0
    goto 20
iload_2            // i
iload_2            // i
iinc 2 by 1        // i++
...
invokevirtual #sb.append()
...
invokevirtual #sb.toString()
// ✅ 只创建了 1 个 StringBuilder 和 1 个最终 String

方式二:StringBuffer(线程安全版)

java
public class StringBufferConcat {
    public static void main(String[] args) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < 100; i++) {
            sb.append(i);
        }
    }
}

StringBufferStringBuilder 的区别:

维度StringBuilderStringBuffer
线程安全不安全安全(synchronized)
性能慢(有同步开销)
适用场景单线程、局部变量多线程、共享变量

方式三:String.concat()

java
public class ConcatMethod {
    public static void main(String[] args) {
        String a = "hello";
        String b = "world";
        String c = a.concat(b);  // 直接调用 concat
    }
}

concat() 底层:

java
public String concat(String str) {
    if (str.isEmpty()) {
        return this;
    }
    int len = value.length;
    byte[] buf = Arrays.copyOf(this.value, len + str.value.length);
    // ...
    return new String(buf, true);
}

特点:如果拼接次数固定(2-3 个),concat()+ 差不多快。但拼接次数不固定时,StringBuilder 更好。

方式四:JDK 15+ 的 String优化的 Indified String Concat

JDK 15 引入了调用点身份(Invokedynamic)优化的字符串拼接:

java
public class IndifiedConcat {
    public static void main(String[] args) {
        // JDK 15+ 中,编译器可能使用 invokedynamic
        // 而不是 StringBuilder
        // 性能更好,因为 JIT 可以做更多优化
        String result = "a" + "b" + "c" + "d";
    }
}

原理:JDK 15 引入 MethodHandle 机制,让 JIT 编译器可以在运行时选择最优的拼接策略,而不是编译时就固定用 StringBuilder。

效率对比

java
public class ConcatBenchmark {
    public static void main(String[] args) {
        // 场景一:固定 3 个字符串拼接
        // 性能差异:+ / concat / StringBuilder 差不多
        String s1 = "a" + "b" + "c";
        String s2 = "a".concat("b").concat("c");

        // 场景二:循环中拼接
        // 强烈推荐 StringBuilder
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 1000; i++) {
            sb.append(i);
        }

        // 场景三:大量小字符串合并
        // 推荐 String.join()
        String[] parts = { "a", "b", "c", "d" };
        String joined = String.join(",", parts);

        // 场景四:Java 12+ 文本块
        // String text = """
        //     line1
        //     line2
        //     """;
    }
}

性能对比表

方式适用场景性能推荐度
+ 拼接固定次数(1-3 次),简单场景一般⭐⭐⭐
String.concat()固定 2-3 个字符串较好(无额外开销)⭐⭐⭐⭐
StringBuilder循环拼接、动态拼接⭐⭐⭐⭐⭐
StringBuffer多线程共享字符串较好(线程安全)⭐⭐⭐
String.join()批量合并数组/列表⭐⭐⭐⭐⭐
List.collect(Collectors.joining())Stream 场景⭐⭐⭐⭐

实战建议

java
public class ConcatBestPractice {
    public static void main(String[] args) {
        // ❌ 循环中使用 +
        String bad = "";
        for (String s : list) {
            bad += s;  // 每次循环都创建 StringBuilder 和 String
        }

        // ✅ 循环中使用 StringBuilder
        StringBuilder good = new StringBuilder();
        for (String s : list) {
            good.append(s);
        }

        // ✅ JDK 8+ 批量合并
        String joined = String.join(",", list);

        // ✅ JDK 8+ Stream 合并
        String streamJoined = list.stream()
            .collect(Collectors.joining(","));

        // ✅ 初始化时确定大小(避免扩容)
        StringBuilder sized = new StringBuilder(list.size() * 10);
    }
}

本节小结

字符串拼接的核心要点:

方式原理性能
+ 拼接编译器转换为 new StringBuilder().append().toString()循环中很差
StringBuilder可变字符数组,追加不重建
StringBufferStringBuilder 的线程安全版本一般(有同步)
String.concat()直接创建新数组固定次数时好
String.join()内部使用 StringBuilder批量合并推荐

+ 拼接在循环中是性能杀手。记住这个原则:循环中用 StringBuilder,批量合并用 String.join()

下一节,我们来看 intern() 方法(原理/面试题/练习)

基于 VitePress 构建