Java中String不可变性本身不慢,真正拖慢拼接性能的是每次拼接都新建对象,导致大量临时对象、频繁GC和字符数组复制,循环拼接时间复杂度达O(n²),应改用StringBuilder或占位符日志。

Java 中 String 的不可变性本身不慢,真正拖慢拼接性能的是“每次拼接都得新建对象”这个必然结果。
每次 + 都在悄悄创建一堆临时对象
String 所有看似修改的操作(+、concat()、substring()、replace())都不改原对象,而是返回新 String。编译器对 "a" + "b" 这类纯字面量会优化成常量池单个字符串;但一旦含变量,比如 s + "x",就会被编译成:
- new StringBuilder().append(s).append("x").toString()
- 每次循环执行,就新建一个 StringBuilder + 一个 String
- 10 万次拼接 ≈ 10 万个 StringBuilder + 10 万个 String 对象
- 大量短命对象推高 GC 频率,CPU 时间花在分配和回收上,而非业务逻辑
底层拷贝开销比想象中更实在
String 内部是 private final char[] value(Java 8)或 byte[] + coder(Java 9+),所有“生成新串”的操作都得复制字符数据:
- "a".concat("b"):直接分配新数组,复制两次源内容,无额外对象
- s1 + s2(变量):走 StringBuilder 路径,多了对象创建+扩容判断+toString()封装
- JDK 7u6 后 substring() 不再共享原数组,改为独立拷贝——安全了,但每次调用都有确定的内存与时间成本
高频拼接场景下,算法复杂度悄然恶化
在循环里写 result += item,表面看是 O(n),实际是 O(n²):
立即学习“Java免费学习笔记(深入)”;
- 第 1 次:分配长度为 len("a") 的数组
- 第 2 次:分配长度为 len("a"+"b") 的数组,复制全部旧内容
- 第 i 次:复制前 i−1 次累计的所有字符 → 累计复制量 ≈ n²/2
- StringBuilder 则只在容量不足时才扩容(通常翻倍),复制次数极少,接近 O(n)
日志等“看起来 harmless”的地方也容易踩坑
像 log.debug("id=" + id + ", name=" + name) 这类写法,即使日志级别关闭,拼接仍会执行:
- 字符串构造照常发生,GC 压力照常产生
- 应改用占位符形式:log.debug("id={}, name={}", id, name),仅当需输出时才真正拼接
- 或者用 Supplier 延迟计算:log.debug(() -> "id=" + id + ", name=" + name)



















