在 Java 开发中,字符串拼接几乎无处不在。
但是很多人依然搞不清:
"a" + "b"
和StringBuilder.append()
性能差多少?StringBuffer
和StringBuilder
到底该选哪个?- 为什么有时
+
也能很快?
本文将从原理、性能、线程安全、最佳实践四个方面,帮你彻底搞懂这个问题。
1. 三者的基本认识
1.1 +
运算符
- 是 Java 的语法糖。
- 在编译期 ,
javac
会将非循环内的字符串常量拼接优化为一个常量。 - 在运行期 ,
+
会被编译器转成使用StringBuilder
(早期版本是StringBuffer
,JDK5 后改成StringBuilder
)。 - 本质上
+
并不是直接的"字符串相加",而是 创建StringBuilder
→ append → toString。
1.2 StringBuilder
- 可变 的字符序列(
String
是不可变的)。 - 非线程安全,但速度快。
- 适用于单线程、多次拼接的场景。
- API:
append()
、insert()
、delete()
等。
1.3 StringBuffer
- 和
StringBuilder
类似,但线程安全 (方法上有synchronized
)。 - 在多线程并发修改同一字符串时安全,但性能比
StringBuilder
略低。 - 单线程下不推荐使用。
2. 工作原理 & 编译器优化
2.1 常量折叠(编译期优化)
java
String s = "a" + "b" + "c";
System.out.println(s); // abc
编译后字节码中会直接变成:
java
String s = "abc";
没有运行期开销。
2.2 循环内拼接
java
String s = "";
for (int i = 0; i < 1000; i++) {
s = s + i;
}
编译器会转成:
java
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(s).append(i);
s = sb.toString();
}
- 每次
toString()
都会创建新String
对象 → 大量内存浪费,性能差。
2.3 手动 StringBuilder
java
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
String s = sb.toString(); // 一次性生成
- 只有一次
toString()
调用,性能更优。
3. 性能对比
一个简单的 10000 次拼接测试(单线程):
方法 | 时间 (ms) | 特点 |
---|---|---|
+ (循环内) |
400+ | 大量中间对象 |
StringBuilder |
3~5 | 性能最佳 |
StringBuffer |
8~10 | 线程安全,稍慢 |
(测试环境:JDK 17,本地单线程,结果随机器差异)
4. 线程安全问题
场景 | 选择 |
---|---|
单线程 | StringBuilder (性能优先) |
多线程,同一实例被多个线程修改 | StringBuffer (保证同步) |
多线程,但每个线程有自己实例 | StringBuilder (无共享状态) |
5. 最佳实践建议
-
能用常量直接拼接就用常量(编译期优化,无运行时开销)。
-
循环中多次拼接 → 直接用
StringBuilder
。 -
多线程共享可变字符串 → 用
StringBuffer
。 -
拼接规模已知 → 初始化
StringBuilder
容量:javaStringBuilder sb = new StringBuilder(1024);
避免频繁扩容。
6. 小结对比表
特性 | + |
StringBuilder |
StringBuffer |
---|---|---|---|
语法简洁 | ✅ | ❌ | ❌ |
性能(循环内) | ❌ | ✅ | 中等 |
线程安全 | ❌ | ❌ | ✅ |
适用场景 | 少量拼接 | 单线程多拼接 | 多线程共享 |
✅ 一句话记忆:
少量拼接用
+
,单线程多次拼接用StringBuilder
,多线程共享用StringBuffer
。