循环中用+拼接字符串性能极差,因string不可变导致频繁创建对象和gc;应优先用预设容量的stringbuilder,单线程场景禁用stringbuffer;string.join()适用于固定分隔符集合拼接,复杂逻辑需组合stringbuilder与formatted()。

Java 字符串拼接不是“用 + 就完事”的问题,尤其在循环或高频场景下,+ 会因创建大量临时 String 对象导致 GC 压力陡增、性能骤降——这是多数人踩过却没深究的坑。
为什么循环里用 + 拼接字符串很危险
Java 中 String 是不可变的,每次 + 都会新建对象。JDK 8+ 虽在编译期对**静态字符串拼接**(如 "a" + "b" + "c")自动优化为常量池引用,但运行时变量拼接(如 str += s 在 for 循环中)仍等价于反复调用 new StringBuilder().append(...).toString(),每次迭代都新建 StringBuilder 实例,开销翻倍。
- 现象:10 万次循环拼接,
+耗时可能是StringBuilder的 5–10 倍,且 Full GC 频次明显上升 - 关键点:JIT 并不会把运行时的
+=循环自动优化成单个StringBuilder - 反例:
String result = ""; for (String s : list) { result += s; // ❌ 每轮都 new StringBuilder + toString }
StringBuilder 和 StringBuffer 怎么选
二者底层都是可变字符数组,但线程安全策略不同:前者非同步,后者所有 public 方法加了 synchronized。绝大多数业务场景(如 Web 请求处理、本地批量组装)无需线程安全,盲目用 StringBuffer 白白牺牲 10%–20% 吞吐量。
- 用
StringBuilder:单线程、性能敏感(如日志组装、JSON 字段拼接) - 用
StringBuffer:多线程共享同一实例且无法隔离(极少见,通常应改用局部变量) - 别忘预设容量:若能预估最终长度,构造时传入容量可避免数组扩容(如
new StringBuilder(1024)),否则默认 16,频繁Arrays.copyOf代价不小
Java 8+ 的 String.join() 和 Java 15+ 的 String.indent() 适用边界
String.join() 是为「已知集合、统一分隔符」场景设计的语法糖,本质就是内部用 StringBuilder 实现,干净高效;但它不解决动态条件拼接或格式化问题。
立即学习“Java免费学习笔记(深入)”;
- 适合:
List<String> parts = Arrays.asList("a", "b", "c"); String joined = String.join("-", parts); // → "a-b-c" - 不适合:需要 if 判断某段是否加入、或需插入换行/缩进等格式控制
-
String.indent()(Java 15+)专用于多行字符串缩进调整,和拼接无直接关系,但常配合String.formatted()或StringBuilder使用,比如生成带缩进的 XML 片段
复杂拼接场景推荐组合:StringBuilder + formatted() + 局部变量
当拼接含变量插值、条件分支、多级缩进时,硬塞进 StringBuilder.append() 链会难读易错。更稳的写法是:用局部 StringBuilder 控制生命周期 + String.formatted() 处理单行模板 + 显式条件分支。
- 示例(生成 SQL WHERE 子句):
StringBuilder where = new StringBuilder(); where.append("WHERE 1=1"); if (userId != null) { where.append(" AND user_id = ").append(userId); } if (status != null) { where.append(" AND status = '").append(status).append("'"); } String sql = "SELECT * FROM orders " + where; - 避免把整个 SQL 写成一行模板(如
"SELECT * FROM ... WHERE %s".formatted(where.toString())),因为formatted()无法处理空值或特殊字符转义 - 注意:不要在循环外复用同一个
StringBuilder实例并反复.setLength(0),除非你严格控制其作用域,否则易引发并发或残留数据问题
真正影响性能的从来不是“用哪个 API”,而是“是否理解每个 API 的内存行为和适用前提”。比如 String.join() 看似简洁,但若传入一个包含百万元素的 ArrayList,它仍会遍历两次(一次算总长,一次拼接)——这时候手写带预估容量的 StringBuilder 反而更可控。










