☕ 别再无脑 new StringBuilder() 了!看懂这个扩容机制,代码效率翻倍

61 阅读4分钟

#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,你都为系统的性能做了一次微小但确定的贡献。