先说说核心结论:给 StringBuilder 指定初始容量,说白了就是为了省去频繁扩容带来的性能开销。扩容这事儿,说白了就是数组拷贝,一次两次还好,次数多了,性能损耗就上来了。提前预估好大小,就能把扩容次数降到最低,甚至直接归零。

那么,具体怎么做?
预估总长度,一步到位
最直接的办法,就是在创建 StringBuilder 之前,把最终字符串的长度算个大概,然后直接传给构造方法。比如你要拼接固定字符串和变量,可以手动加一加:
- 已知的字面量长度,比如
"Hello "就是 6 个字符 - 变量的话,如果是个对象,可以调
.toString().length();如果是基本类型,按经验估算,比如int最多 11 位,long最多 20 位 - 别忘了分隔符、换行符这些小东西,加起来就对了
举个例子:new StringBuilder(6 + name.length() + 4 + ageStr.length()),对应 "Name: " + name + ", Age: " + age 这种拼接。这样,内部数组从一开始就够用,性能自然最优。
用 ensureCapacity() 兜底
如果构造时实在没法精确估算,比如逻辑比较复杂,那可以在拼接之前主动调用 ensureCapacity(int minimumCapacity)。这个方法会强制保证内部数组至少有指定容量,不够的话一次扩容到目标大小:
- 比边拼边扩容更可控,不会出现“满了才扩,扩了又满”的尴尬局面
- 特别适合“先收集数据,再统一拼接”的场景
- 注意,它只影响
capacity(),不影响length(),也就是不会改变已有的内容
警惕过度预留,别浪费内存
容量设大固然能防扩容,但也不是越大越好。设得太大,尤其在大量短生命周期对象的场景下,堆内存压力会明显上升:
- 不要盲目设个几千上万,除非你真需要那么多
- 对于不确定长度的场景,可以结合经验值,比如日志消息通常不超过 256 字符,那设个 256 就够了
- JVM 对小对象的内存分配优化其实不错,适度预留(比如 64、128、256)往往性价比更高
看看默认容量,就明白扩容代价了
StringBuilder() 默认容量是 16。一旦超出,JDK 8 的扩容公式是 newCapacity = oldCapacity * 2 + 2,JDK 17+ 则是 oldCapacity + (oldCapacity >> 1),也就是 1.5 倍。连续扩容意味着多次数组复制,每次都要把已有内容全部拷贝一遍:
- 从 16 开始,要扩到 1000 字符,大概得扩容 6 次,每次拷贝的数据量越来越大
- 如果一开始就设成 1024,那零扩容,性能差距非常明显