#Java #StringBuilder #源码分析 #面试避坑
摘要:
大家都知道拼接字符串要用 StringBuilder,但你知道它底层是怎么“长大”的吗?默认容量 16 满载后,下一个容量是多少?17?32?还是 34?本文带你扒开 JDK 8 源码,看清它的扩容黑幕。
我们在写 Java 代码时,只要涉及字符串拼接,大家都会下意识地用 StringBuilder。毕竟比直接用 + 号性能好太多。
但你有没有想过:它是怎么管理内存的?当 16 个格子满了,它是怎么申请新房子的?
来看这道经典的“送命题”:
🛑 灵魂拷问:容量变多少?
// 1. 默认初始容量是 16
StringBuilder sb = new StringBuilder();
// 2. 假设我们已经追加了 16 个字符,现在内部数组满了
sb.append("0123456789012345");
// 3. 再追加 1 个字符,触发扩容
sb.append("a");
// ❓ 提问:这时候,sb 的内部容量 (Capacity) 会变成多少?
-
A. 17 (够用就行)
-
B. 32 (直接翻倍)
-
C. 34 (玄学数字)
✅ 深度解析:为什么是 34?
你的答案:A (17) ?觉得 16+1 刚刚好?
正确答案:C (34)
你选 17 可能是觉得“够用就行”。但 Java 的设计者为了减少频繁扩容(搬家很累的,底层需要 Arrays.copyOf 迁移整个数组,非常耗性能),采用了一种**“倍增 + 2”**的策略。
别猜了,直接看 JDK 8 的源码 (AbstractStringBuilder.java),逻辑很简单:
void ensureCapacityInternal(int minimumCapacity) {
if (minimumCapacity - value.length > 0) {
// 1. 尝试计算新容量:原容量 * 2 + 2
int newCapacity = (value.length << 1) + 2;
// ... (兜底逻辑见下文)
// 2. 开始搬家
value = Arrays.copyOf(value, newCapacity);
}
}
看懂了吗?
-
value.length是当前容量 16。 -
<< 1是位运算(左移一位),相当于 乘以 2,变成 32。 -
+ 2是为了防止 0 容量死循环等边界情况,最后变成 34。
这多出来的 +2,就是让很多老手翻车的细节。
🚀 进阶:不够装怎么办?
面试官可能会突然“坏笑”一下追问:“那如果我这一次 append 了一万字,翻倍 + 2 也不够装怎么办?”
这才是源码精髓!Java 肯定没那么傻。
假设你初始 16,突然塞进来 50 个字符。
-
需要:16 + 50 = 66。
-
翻倍算出来:34。
-
34 < 66,显然不够。
这时候,源码里紧接着有一段霸气兜底逻辑:
// ⚠️ 还是不够?那就你要多少给多少!
if (newCapacity - minimumCapacity < 0) {
newCapacity = minimumCapacity;
}
结论就是:
先试着“翻倍+2”;如果还不够,那就“实报实销”**,直接扩容到你需要的长度。
🛠️ 实战避坑指南
既然看透了机制,我们写代码就能更专业了。与其让 JVM 自己瞎折腾,不如我们手动优化:
1. 预估容量(最推荐)
如果你知道大概要拼接 200 个字,千万别用默认构造函数。
// ✅ 一次分配到位,全程 0 扩容,0 搬家
StringBuilder sb = new StringBuilder(200);
2. 别过度预分配
也别太贪心,比如你只要拼几十个字,却 new StringBuilder(10000),那是纯纯的浪费内存。
3. 循环中的优化
这是最容易被忽视的:
// ✅ 建议在循环外创建,并给个大概的容量
StringBuilder sb = new StringBuilder(items.size() * 10);
for (String item : items) {
sb.append(item);
}
4. 链式调用
链式调用(.append().append())很帅,而且不会改变扩容逻辑。每次 append 都会检查容量,该扩容还是会扩容。
🔍 知识延伸:StringBuilder vs ArrayList
这一块很有意思,同是扩容,Java 的亲儿子们性格还不一样:
-
StringBuilder / StringBuffer:性格豪爽,扩容大概是 2 倍 (
*2 + 2),为了更快的追加字符串。 -
ArrayList:性格相对保守,扩容是 1.5 倍 (
old + old >> 1),为了节省内存。
总结一下:
下次面试再问到这个,记住“翻倍+2”口诀,再甩出“兜底逻辑”,最后补充一句“预设容量才是最佳实践”,这波稳了!😎
📝 总结
StringBuilder 的扩容机制,本质上是一场空间换时间的博弈:
-
平时:用“翻倍+2”的激进策略,尽量减少“搬家”(数组拷贝)的次数,保全性能。
-
急时:用“实报实销”的兜底逻辑,确保再大的数据也能装得下。
给开发者的最终建议: 虽然 JVM 帮我们做了很多优化,但最好的优化永远在代码写出来之前。 下次写 new StringBuilder() 时,不妨多想一步:“我大概需要多少容量?” 哪怕只是填个 128,你都为系统的性能做了一次微小但确定的贡献。