String、StringBuilder 和 StringBuffer 的区别?
结论:三者都实现 CharSequence,真正的差别只有两点——可变性(mutability)与线程安全(thread-safe)。String 不可变,任何"修改"都返回新对象;StringBuilder 可变、不加锁、单线程最快;StringBuffer 可变、公开方法带 synchronized,线程安全但每次操作都有锁开销。选型口诀:常量和少量拼接用 String,单线程循环拼接用 StringBuilder,多线程共享同一缓冲区才考虑 StringBuffer(这种场景现在很少见)。
1. 一张表看清差别
| 维度 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 可变性 | 不可变(immutable) | 可变 | 可变 |
| 线程安全 | 安全(对象不可变,天然共享) | 不安全 | 安全(方法级 synchronized) |
| 拼接代价 | 每次产生新对象 | 原地 append,均摊 O(1) | 原地 append + 加锁 |
| 适用场景 | 常量、缓存 key、少量拼接 | 单线程大量拼接 | 多线程共享同一缓冲区 |
| 首次出现 | JDK 1.0 | JDK 1.5 | JDK 1.0 |
| 直接父类 | Object | AbstractStringBuilder | AbstractStringBuilder |
关键点:StringBuilder 与 StringBuffer 都继承 AbstractStringBuilder,底层数组与扩容逻辑完全一致,后者只是在对外方法上加了锁,所以两者性能差距主要来自锁的获取与释放。
2. 内部结构:为什么可变对象快
String: [ final byte[] value ][ coder ][ hash ] ← 内容不可改
StringBuilder: [ byte[] value ][ count ][ coder ]
├── 0 ~ count-1 已写入
└── count ~ capacity-1 空闲,append 直接写这里
容量不足时:newCapacity = (oldCapacity << 1) + 2,Arrays.copyOf 搬到新数组
JDK 9 引入紧凑字符串(compact strings)后,String 与 AbstractStringBuilder 内部都改成 byte[] value 加一个 byte coder:纯 Latin-1 内容每字符占 1 字节,含中文等字符时用 UTF-16 每字符 2 字节。这也解释了为什么"同样长度的英文串占用的内存比 JDK 8 少一半"。
append 之所以快,是因为它只做两件事:检查剩余容量、把字符写进 value[count++],不创建新对象(除非扩容)。而 String 的 + 每次都要分配一个新数组并复制全部内容,循环里用 += 就会退化成 O(n²)。
3. 代码:扩容与循环拼接
public class BuilderDemo {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder(); // 默认容量 16
sb.append("abc");
System.out.println(sb.length()); // 3
System.out.println(sb.capacity()); // 16
StringBuilder big = new StringBuilder("hello"); // 容量 = 5 + 16 = 21
big.append(" world, java interview"); // 需要 24 > 21
System.out.println(big.capacity()); // (21 << 1) + 2 = 44
// 反例:循环里用 + 拼接,每轮至少新建一个 StringBuilder 和一个结果 String
String s = "";
for (int i = 0; i < 10_000; i++) {
s += i;
}
// 正例:复用同一个缓冲区,只在容量不足时扩容
StringBuilder buf = new StringBuilder(16 * 1024); // 预估容量,避免反复扩容
for (int i = 0; i < 10_000; i++) {
buf.append(i);
}
String result = buf.toString(); // 这里才产生最终 String
}
}
预估容量是最容易被忽略的优化:一次分配够,胜过十次翻倍复制。反过来说,new StringBuilder() 在拼接结果较大时会经历 16 → 34 → 70 → 142 这样的多次扩容,每次都复制一遍旧内容。
4. 常见追问
| 追问 | 回答要点 |
|---|---|
StringBuilder 线程不安全具体表现是什么? | count++ 与数组写入不是原子操作,并发 append 会丢字符、内容错乱,极端情况下数组越界 |
那用 StringBuffer 代替就行吗? | 不行。正确做法是每个线程一个 StringBuilder;StringBuffer 只能保证单次方法调用原子,复合操作仍要外部加锁 |
StringBuffer 真的线程安全吗? | 单方法安全,"先判断再拼接"这类复合逻辑不安全,仍需要 synchronized |
toString() 有开销吗? | 有,返回一份新的不可变副本,O(n) 拷贝;不要在循环里反复 toString() 比较 |
JDK 9 之后 + 还用 StringBuilder 吗? | 不用,改成 invokedynamic + StringConcatFactory,一次估算长度直接生成结果,但循环里仍然每轮新建 |
capacity 和 length 什么区别? | length 是已写入字符数,capacity 是数组容量,capacity >= length |
还有两个容易答错的细节:
StringBuilder没有重写equals/hashCode,比较两个StringBuilder内容是否相同必须toString().equals(...),直接用==比的是引用。StringBuilder可以setLength(0)复用缓冲区,比反复new更省;但复用时要注意别把旧数据带进新结果。
小结
String 用不可变换来常量池共享与线程安全,StringBuilder 用可变数组换拼接性能,StringBuffer 用锁换线程安全。面试时别只背"一个可变一个不可变"——要能说出扩容公式、+ 的编译产物、以及为什么 StringBuffer 在单线程下不划算。