编译期字符串拼接由javac直接优化为常量;局部变量单行拼接自动转为stringbuilder;jdk9+用invokedynamic动态优化;循环内s+=str仍为o(n²)低效,须显式用预估容量的stringbuilder。

编译期常量拼接:根本不用 StringBuilder
当所有参与 + 的操作数都是字符串字面量(如 "a" + "b" + "c"),javac 会在编译阶段直接计算出结果 "abc",并写入 class 文件的常量池。字节码里只有一条 ldc 指令加载这个合并后的字符串,完全不出现 StringBuilder、append 或任何对象创建指令。
这种优化依赖于字符串的不可变性和编译期可确定性。只要中间掺一个变量(哪怕 final String x = "foo"; 在某些 JDK 版本中也不稳定),就立刻退出该路径。
局部变量单行拼接:编译器帮你 new StringBuilder
像 String s = a + b + c;(其中 a、b、c 是方法参数或普通局部变量)这样的语句,javac 会重写为等效的 new StringBuilder().append(a).append(b).append(c).toString()。
注意几个关键点:
- 仅限**同一语句内**;拆成多行、加 if/else 分支、或跨循环体,都不触发此优化
- 默认初始容量是 16,如果拼接总长度远超这个值,运行时仍会触发扩容(复制数组)
- 生成的是
StringBuilder,不是StringBuffer;JDK 8 及以前字节码清晰可见构造和 append 调用
JDK 9+ 的 InvokeDynamic 拼接:编译器不写死逻辑了
从 JDK 9 开始,类似 String s = "prefix" + value + "suffix"; 的拼接,字节码不再显式调用 StringBuilder,而是用一条 invokedynamic 指令替代。
这条指令背后由 StringConcatFactory 在首次执行时动态生成最优策略,可能包括:
-
MH_INLINE_SIZED_EXACT:提前算好最终长度,一次性分配 byte[],避免扩容 - 基于
StringBuilder的兜底策略(仍比手动写更智能) - 对 null 值的统一处理逻辑(比如转成字符串
"null")
你无法在源码或字节码里看到 StringBuilder,但它依然存在——只是被 JIT 和链接层封装掉了。
循环里写 s += str:别信“自动优化”,它很慢
这是最容易踩坑的地方。写 for (String x : list) s += x;,无论 JDK 8 还是 JDK 17,每轮迭代都相当于执行:
new StringBuilder(s).append(x).toString()
也就是说,每次都要新建 StringBuilder、拷贝旧字符串内容、追加新内容、再生成新 String。时间复杂度 O(n²),内存压力陡增。
即使 JDK 9+ 有 invokedynamic,它也**不适用于循环体内重复拼接场景**——每个 += 都是独立指令,无法复用中间状态。
真正可控的做法永远是:
- 显式声明
StringBuilder sb = new StringBuilder(预估容量); - 循环中只调
sb.append(x) - 最后统一
sb.toString()
预估容量不是可选项——没预设容量时,16 字符起步,频繁扩容复制的开销在大数据量下非常真实。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











