在Java开发中,字符串处理是最常见的操作之一。然而,很多开发者对 String、StringBuffer 和 StringBuilder 的区别仅停留在"可变/不可变"或"线程安全/非线程安全"的表面理解上。本文将从底层源码实现 出发,深入剖析三者的内部机制,并结合最佳实践 与常见陷阱,帮助你彻底掌握它们的使用之道。
一、核心区别概览
| 特性 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 | 不可变(Immutable) | 可变(Mutable) | 可变(Mutable) |
| 线程安全 | 是(因不可变) | 是(synchronized) | 否 |
| 底层存储 | final char[] value |
char[] value(非final) |
char[] value(非final) |
| 性能 | 拼接性能差(频繁创建新对象) | 中等(同步开销) | 最高(无同步) |
| 适用场景 | 常量、少量拼接 | 多线程环境下的字符串构建 | 单线程下的高性能拼接 |
二、底层源码深度解析
1. String:不可变性的根源
java
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final byte[] value; // Java 9+ 使用 byte[] + coder 字段优化内存
// Java 8 及之前:private final char[] value;
// 所有修改操作(如 concat, replace)都返回新 String 对象
public String concat(String str) {
if (str.isEmpty()) return this;
return new String(value, true).concat(str); // 实际创建新对象
}
}
关键点:
final类 +final字段 → 不可变- 任何"修改"操作都会创建新对象,旧对象保留在常量池或堆中
- Java 9 起,为节省内存,
String内部改用byte[]存储,并通过coder字段标识是 LATIN1 还是 UTF16 编码
📌 不可变性的好处:线程安全、可缓存(字符串常量池)、可用作 HashMap 的 key。
2. StringBuffer:线程安全的可变字符串
java
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence {
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
}
- 继承自
AbstractStringBuilder - 所有 public 方法都加了
synchronized→ 线程安全 - 内部使用
char[] value(非 final),可动态扩容
3. StringBuilder:高性能的单线程选择
java
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence {
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
}
- 同样继承
AbstractStringBuilder - 方法无
synchronized→ 非线程安全,但性能更高 - 与
StringBuffer共享大部分逻辑(如扩容策略)
4. AbstractStringBuilder:共享的核心逻辑
StringBuffer 和 StringBuilder 的核心实现在 AbstractStringBuilder 中:
java
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count; // 当前字符数
public AbstractStringBuilder append(String str) {
if (str == null) str = "null";
int len = str.length();
ensureCapacityInternal(count + len); // 扩容检查
str.getChars(0, len, value, count); // 复制字符
count += len;
return this;
}
private void ensureCapacityInternal(int minimumCapacity) {
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value, newCapacity(minimumCapacity));
}
}
private int newCapacity(int minCapacity) {
int newCapacity = (value.length << 1) + 2; // 扩容为原长度*2+2
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
return newCapacity;
}
}
扩容策略 :
默认容量 16,当空间不足时,新容量 = 原容量 * 2 + 2。若仍不够,则直接使用所需最小容量。
三、性能对比实验
java
// 测试:拼接 10 万次 "a"
long start = System.currentTimeMillis();
// 方式1:String +=
String s = "";
for (int i = 0; i < 100_000; i++) s += "a"; // 极慢!O(n²)
// 方式2:StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100_000; i++) sb.append("a"); // 快!
// 方式3:StringBuffer
StringBuffer buf = new StringBuffer();
for (int i = 0; i < 100_000; i++) buf.append("a"); // 比 StringBuilder 慢约 10~30%
System.out.println(System.currentTimeMillis() - start);
结果(JDK 17,典型值):
String +=:> 10,000 ms(不推荐)StringBuilder:≈ 5 msStringBuffer:≈ 7 ms
💡 注意:现代编译器(如 JDK 8+)会对局部变量 的
+=操作自动优化为StringBuilder,但跨方法或循环内多次拼接仍会退化。
四、最佳实践与避坑指南
✅ 正确使用姿势
-
字符串常量/少量拼接 → 用
StringjavaString msg = "Hello, " + name + "!"; // 编译期优化为 StringBuilder -
单线程大量拼接 → 用
StringBuilderjavaStringBuilder sb = new StringBuilder(1024); // 预估容量避免多次扩容 for (String item : list) sb.append(item).append("\n"); return sb.toString(); -
多线程共享构建 → 用
StringBuffer(但更推荐同步外部控制)java// 更佳方案:用 StringBuilder + 外部 synchronized private final StringBuilder sharedBuilder = new StringBuilder(); public synchronized void append(String s) { sharedBuilder.append(s); }
⚠️ 常见陷阱与避坑
坑1:误以为 String += 总是高效
java
// 错误:在循环中使用 +=
String result = "";
for (int i = 0; i < n; i++) {
result += items[i]; // 每次都 new StringBuilder + toString()
}
✅ 修复 :改用 StringBuilder
坑2:忽略初始容量导致频繁扩容
java
// 默认容量16,若拼接1000字符,会扩容多次
StringBuilder sb = new StringBuilder();
✅ 修复:预估大小
java
StringBuilder sb = new StringBuilder(expectedSize);
坑3:在多线程中误用 StringBuilder
java
// 多线程并发 append 可能导致数组越界或数据错乱!
static StringBuilder sb = new StringBuilder();
✅ 修复 :改用 StringBuffer 或线程局部变量(ThreadLocal<StringBuilder>)
坑4:混淆 equals() 行为
java
StringBuffer sb1 = new StringBuffer("hello");
StringBuffer sb2 = new StringBuffer("hello");
System.out.println(sb1.equals(sb2)); // false!因为未重写 equals()
✅ 注意 :StringBuffer/StringBuilder 的 equals() 是引用比较,不要用于内容比较 。应转为 String 后再比较:
java
sb1.toString().equals(sb2.toString());
五、总结
| 场景 | 推荐类型 |
|---|---|
| 字符串常量、配置项、key | String |
| 单线程内大量拼接(日志、JSON 构建等) | StringBuilder(带初始容量) |
| 多线程共享且必须在线程内拼接 | StringBuffer(但优先考虑外部同步) |
| 需要内容比较 | 统一转为 String 后使用 equals() |
核心原则:
- 不可变用
String,可变拼接用StringBuilder,线程安全需求才考虑StringBuffer - 永远不要在循环中用
String +=拼接 - 预估容量,减少扩容开销
- 多线程下慎用
StringBuilder
掌握这些底层原理与实践技巧,你就能在字符串处理上写出高性能、无 bug 的 Java 代码!
📚 延伸阅读:
- 《Java Performance: The Definitive Guide》
- OpenJDK 源码:
java.lang.String,java.lang.AbstractStringBuilder- JEP 254: Compact Strings(Java 9 字符串内存优化)
作者 :架构师Beata
日期 :2026年2月9日
声明 :本文基于网络文档整理,如有疏漏,欢迎指正。转载请注明出处。
互动:如有任何问题?欢迎在评论区分享!