本文目录导读:

这个问题的核心在于 String 的不可变性,而 StringBuilder 是可变的,在大量字符串拼接的场景下,两者在内存和性能上的表现天差地别。
以下是详细的技术对比:
核心原理:不可变 vs 可变
- String 是不可变对象:一旦创建,其内部字符数组
final char[]就无法修改。- 拼接操作的实际含义:
"A" + "B" + "C"在编译或运行时,会创建一个新的 String 对象("ABC"),而原来的"A"、"B"和中间过程的"AB"对象依然存在于内存中,等待被垃圾回收。
- 拼接操作的实际含义:
- StringBuilder 是可变对象:内部维护一个可扩容的字符数组。
- 拼接操作的实际含义:
sb.append("A").append("B").append("C")操作是直接在底层数组的末尾追加字符,如果数组容量不够,就进行扩容(通常是旧容量 * 2 + 2),整个过程最多产生一个 StringBuilder 对象,以及最终toString()时的一个 String 对象。
- 拼接操作的实际含义:
性能差异的具体数据表现
为了直观理解,以拼接 n 个字符串为例:
时间复杂度对比
| 操作 | 时间复杂度 | 原因 |
|---|---|---|
String 拼接 |
O(n²) | 每次拼接都需要将原字符串和待拼接字符串复制到一个新的字符数组中,第1次复制1个字符,第2次复制2个...总共复制次数约为 n(n+1)/2。 |
StringBuilder 拼接 |
O(n) | 大部分情况下直接在尾部追加;当数组满时偶尔扩容,但扩容的均摊成本是 O(1)。 |
内存/对象创建对比
String方式:拼接n次,会创建约n+1个 String 对象(包括中间结果),这些对象占用大量内存,并给 GC(垃圾回收)带来巨大压力。StringBuilder方式:通常只创建 1 个 StringBuilder 对象,加上最后调用toString()创建的 1 个 String 对象,总共 2 个对象。
典型场景佐证
在循环内进行拼接:
// 错误示范:在循环内使用 + 号
String s = "";
for (int i = 0; i < 1000; i++) {
s += i; // 每次循环都创建新的 String 对象,性能极差
}
// 正确做法:使用 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i); // 一直在同一个对象的数组中追加
}
String result = sb.toString();
前者在实际测试中(循环1万次),执行时间可能是后者的几十倍到上百倍,且会产生大量垃圾对象。
特殊情况:编译器的自动优化
Java 编译器会对简单的、非循环内的 操作进行优化。
// 编译器会将以下代码: String s = a + b + c; // 自动优化为等价于: String s = new StringBuilder().append(a).append(b).append(c).toString();
但是,这种优化有明确的局限性:
- 循环内无效:编译器不会在循环内部自动使用 StringBuilder,它会在每次循环迭代中创建一个新的
StringBuilder,仍然效率极低,以上面的循环例子为例,编译器相当于把它变成了:for (int i...) { s = new StringBuilder().append(s).append(i).toString(); // 仍然很慢 } - 初始化容量未知:编译器生成的
StringBuilder使用的是默认容量(16),如果没有手动指定合适的初始容量,可能会导致多次扩容(数组复制),影响性能。
什么时候应该用 String?
虽然 StringBuilder 在拼接上优势明显,但 String 也有其适用场景:
- 极少量、固定的字符串拼接:如
"Hello, " + name,这种写法简洁、可读性高,性能开销可以忽略不计。 - 参数传递、配置、固定常量:String 的不可变性意味着它是线程安全的,适合作为哈希键(HashMap的Key)。
- 不需要修改的场景:如日志输出、返回固定消息。
| 场景 | 推荐使用 | 主要原因 |
|---|---|---|
| 大量(>10次)拼接 | StringBuilder |
避免大量对象创建和内存复制,性能显著提升 |
| 循环内拼接 | StringBuilder |
编译器不会在循环内优化,必须手动使用 |
| 简单、少量的拼接 | String ( 号) |
代码简洁,且编译器会优化为 StringBuilder |
| 需要线程安全 | StringBuffer |
线程安全但性能略低于 StringBuilder(实际上很少需要) |
| 作为不可变 Key | String |
不可变性是 HashMap 正常工作的重要保证 |
一句话回答:String 每次拼接都会创建新对象(O(n²) 复杂度),而 StringBuilder 只在底层数组追加(O(n) 复杂度),大量拼接时,String 的方式会造成巨大的性能开销和内存浪费。