字符串拼接(原理/效率对比)
字符串拼接无处不在
"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);
}
}
}StringBuffer 和 StringBuilder 的区别:
| 维度 | StringBuilder | StringBuffer |
|---|---|---|
| 线程安全 | 不安全 | 安全(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 | 可变字符数组,追加不重建 | 好 |
StringBuffer | StringBuilder 的线程安全版本 | 一般(有同步) |
String.concat() | 直接创建新数组 | 固定次数时好 |
String.join() | 内部使用 StringBuilder | 批量合并推荐 |
+ 拼接在循环中是性能杀手。记住这个原则:循环中用 StringBuilder,批量合并用 String.join()。
下一节,我们来看 intern() 方法(原理/面试题/练习)。
