StringBuilder
和 StringBuffer
是 Java 中用于处理可变字符串的两个核心类。它们功能几乎完全相同,但在线程安全和性能上存在关键差异。
一、核心概念
1. 共同点
- 可变字符序列:两者都实现了
Appendable
和CharSequence
接口,支持动态修改字符串内容。 - 内部结构:都基于可变的
char[]
数组实现,支持自动扩容。 - 功能一致:提供
append
、insert
、delete
、replace
、reverse
、toString
等相同的方法集。 - 非 final 类:都可以被继承(尽管通常不建议)。
2. 核心区别
特性 | StringBuilder |
StringBuffer |
---|---|---|
线程安全 | ❌ 不安全 | ✅ 安全 |
同步机制 | 无同步 | 方法使用 synchronized 关键字 |
性能 | ⚡ 更高(单线程) | ⚠️ 较低(因同步开销) |
出现版本 | JDK 1.5 | JDK 1.0 |
适用场景 | 单线程环境 | 多线程共享环境 |
关键结论:在单线程下,优先使用
StringBuilder
;只有在多线程需要共享同一个字符串缓冲区时,才使用StringBuffer
。
二、操作步骤(两者通用,非常详细)
由于 StringBuilder
和 StringBuffer
的 API 完全相同,以下操作步骤适用于两者。
步骤 1:导入类
// 两者都在 java.lang 包中,无需显式导入
// 但为了清晰,可写:
import java.lang.StringBuilder;
import java.lang.StringBuffer;
步骤 2:创建实例
方法 1:无参构造函数
StringBuilder sb = new StringBuilder();
StringBuffer sf = new StringBuffer();
// 初始容量:16 个字符
方法 2:指定初始容量
StringBuilder sb = new StringBuilder(100);
StringBuffer sf = new StringBuffer(100);
// 初始容量为 100,避免频繁扩容
方法 3:基于字符串初始化
StringBuilder sb = new StringBuilder("Hello");
StringBuffer sf = new StringBuffer("Hello");
// 初始内容为 "Hello",容量 = 16 + 字符串长度
步骤 3:追加内容(append
)
基本追加
sb.append("World");
sf.append("World");
// 支持:String, int, boolean, char, double, Object 等
链式调用(推荐)
sb.append("Name: ").append("Alice").append(", Age: ").append(30);
sf.append("Name: ").append("Bob").append(", Age: ").append(25);
步骤 4:插入内容(insert
)
sb.insert(5, " ");
sf.insert(5, " ");
// 在索引 5 处插入空格
// 注意:索引从 0 开始,不能越界
步骤 5:删除内容
删除指定范围
sb.delete(5, 6); // 删除索引 5 到 5([5,6))
sf.delete(5, 6);
删除单个字符
sb.deleteCharAt(5);
sf.deleteCharAt(5);
步骤 6:替换内容(replace
)
sb.replace(6, 11, "Java");
sf.replace(6, 11, "Java");
// 将索引 6 到 10 的字符替换为 "Java"
步骤 7:反转字符串(reverse
)
sb.reverse();
sf.reverse();
// 原地反转字符序列
步骤 8:获取字符串结果(toString
)
String result1 = sb.toString();
String result2 = sf.toString();
// 重要:返回新的 String 对象,原缓冲区仍可修改
步骤 9:查询信息
int len = sb.length(); // 当前字符数
int cap = sb.capacity(); // 当前数组容量
char c = sb.charAt(0); // 获取索引 0 的字符
sb.setCharAt(0, 'h'); // 设置索引 0 的字符
三、常见错误
错误 1:混淆线程安全性
// 错误:在多线程中共享 StringBuilder
StringBuilder sharedSb = new StringBuilder();
// 多个线程同时执行 sharedSb.append(...) → 数据错乱!
// 正确:多线程应使用 StringBuffer
StringBuffer sharedSf = new StringBuffer();
// 或使用 StringBuilder + 外部同步
synchronized(sharedSb) {
sharedSb.append("data");
}
错误 2:忽略 toString()
返回新对象
StringBuilder sb = new StringBuilder("Hello");
String str = sb.toString();
sb.append(" World");
// 此时 str 仍是 "Hello",不会变为 "Hello World"
// 因为 toString() 返回的是快照
错误 3:索引越界
StringBuilder sb = new StringBuilder("Hi");
sb.delete(10, 12); // 抛出 StringIndexOutOfBoundsException
// 解决:检查 length(),确保 start < end <= length()
错误 4:过度依赖自动扩容
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append("a");
}
// 可能触发多次数组复制(扩容)
// 优化:new StringBuilder(10000)
四、注意事项
线程安全是核心区别:
StringBuffer
的每个修改方法(如append
、insert
)都加了synchronized
,保证多线程安全。StringBuilder
无同步,单线程下性能更高。
性能差异来源:
synchronized
会带来锁竞争、上下文切换等开销。- 在单线程下,
StringBuilder
通常比StringBuffer
快 10%-20%。
容量管理:
- 初始容量 16。
- 扩容策略:
newCapacity = (oldCapacity << 1) + 2
(即old * 2 + 2
)。 - 频繁扩容会触发数组复制,影响性能。
toString()
的实现:- 两者都通过
new String(value, 0, count)
创建新字符串,不共享内部数组。
- 两者都通过
不要用于多线程缓存:
- 即使使用
StringBuffer
,在高并发下仍可能成为瓶颈。考虑ThreadLocal<StringBuilder>
或无锁设计。
- 即使使用
五、使用技巧
技巧 1:根据场景选择类
// 场景 1:单线程拼接(如方法内)
StringBuilder sb = new StringBuilder();
sb.append("Processing...").append(id);
// 场景 2:多线程共享(罕见)
StringBuffer sf = new StringBuffer();
// 多个线程调用 sf.append(...)
技巧 2:预设容量提升性能
// 预估最终长度 ≈ 1000
StringBuilder sb = new StringBuilder(1000);
// 避免扩容,提升 10%-30% 性能
技巧 3:ThreadLocal
优化多线程
// 高并发下,避免共享 StringBuffer
private static final ThreadLocal<StringBuilder> tlSb =
ThreadLocal.withInitial(() -> new StringBuilder(1000));
// 使用
StringBuilder sb = tlSb.get();
sb.setLength(0); // 清空复用
sb.append("data");
String result = sb.toString();
技巧 4:链式调用提高可读性
return new StringBuilder()
.append("{")
.append("\"name\":\"").append(name).append("\",")
.append("\"age\":").append(age)
.append("}")
.toString();
技巧 5:清空内容复用对象
sb.setLength(0); // 清空,复用对象(比 new 更快)
// 或 sb.delete(0, sb.length());
六、最佳实践与性能优化
1. 选择原则
场景 | 推荐类 |
---|---|
单线程,频繁拼接 | ✅ StringBuilder |
多线程共享拼接 | ✅ StringBuffer |
多线程,非共享 | ✅ ThreadLocal<StringBuilder> |
简单拼接(<3次) | ✅ String + (编译器优化) |
构建 JSON/XML | ✅ StringBuilder + 预设容量 |
2. 性能对比测试(示例)
// 单线程性能测试
long start = System.nanoTime();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) sb.append("a");
long timeSb = System.nanoTime() - start;
start = System.nanoTime();
StringBuffer sf = new StringBuffer();
for (int i = 0; i < 10000; i++) sf.append("a");
long timeSf = System.nanoTime() - start;
System.out.println("StringBuilder: " + timeSb);
System.out.println("StringBuffer: " + timeSf);
// 通常 StringBuilder 更快
3. 避免 StringBuffer
的陷阱
- 不要滥用:99% 的字符串拼接发生在单线程方法内,用
StringBuilder
即可。 - 高并发瓶颈:
synchronized
方法在高并发下可能成为性能瓶颈。
4. 结合其他工具
StringJoiner
:JDK 8+,适合分隔符拼接。StringJoiner sj = new StringJoiner(","); sj.add("a").add("b");
String.format
:格式化字符串,但性能低于StringBuilder
链式调用。
5. 监控与调优
- 使用 JMH 测试实际性能。
- 监控 GC 频率,频繁字符串操作可能导致 Young GC 增加。
- 考虑对象池(谨慎使用,可能增加复杂度)。
总结
对比项 | StringBuilder |
StringBuffer |
---|---|---|
线程安全 | 否 | 是 |
性能 | 高 | 中 |
推荐场景 | 单线程(99%) | 多线程共享(1%) |
关键方法 | append , insert , delete , toString |
同左 |
最佳实践 | 预设容量、链式调用、复用 | 仅在必要时使用 |
最终建议:
默认使用
StringBuilder
。只有当你明确需要在多个线程间共享同一个可变字符串缓冲区时,才使用StringBuffer
。在绝大多数应用中,StringBuilder
是更优、更高效的选择。