
04 | String 背后发生了什么别让拼接拖垮性能摘要循环里用拼接字符串性能差到让你怀疑人生。本文彻底讲清编译器对的优化边界以及StringBuilder的正确使用时机。一、问题现象publicclassStringConcatTest{publicstaticvoidmain(String[]args){// 方式一用 Strings;longstartSystem.currentTimeMillis();for(inti0;i100000;i){si;// ❌ 性能灾难}System.out.println( 耗时(System.currentTimeMillis()-start)ms);// 方式二用 StringBuilderStringBuildersbnewStringBuilder();startSystem.currentTimeMillis();for(inti0;i100000;i){sb.append(i);// ✅ 性能优秀}System.out.println(StringBuilder 耗时(System.currentTimeMillis()-start)ms);}}运行结果参考值 耗时约 12000ms StringBuilder 耗时约 5ms差距超过 2000 倍二、踩坑现场场景 1循环中字符串拼接// ❌ 错误写法每行日志都创建 StringBuilder toStringStringresult;for(Orderorder:orders){resultorder.getId():order.getStatus(),;}问题每次都会创建一个新的StringBuilder对象然后toString()生成新String产生大量临时对象触发频繁 GC。场景 2SQL 拼接// ❌ 错误写法StringsqlSELECT * FROM user WHERE 11 ;if(name!null){sqlAND name name ;}if(age!null){sqlAND age age ;}// SQL 注入风险 性能问题 双重踩坑三、原理解析3.1 编译器对的优化Java 编译器javac在编译期会对字符串进行优化// 你写的代码非循环场景StringsHelloname!;// 编译器优化后等价于StringBuildersbnewStringBuilder();sb.append(Hello).append(name).append(!);Stringssb.toString();关键结论单个表达式内的拼接编译器会优化成StringBuilder性能没问题。3.2 循环内为什么没被优化// 你写的代码Strings;for(inti0;i10;i){si;}// 编译器处理后的真实代码等价于Strings;for(inti0;i10;i){StringBuildertempnewStringBuilder();temp.append(s).append(i);stemp.toString();// 每次循环都 new 一个 StringBuilder 一次 toString}问题核心编译器把翻译成「新建StringBuilder→append→toString」每次循环都新建对象无法复用。3.3 字节码视角用javap -c查看字节码循环内的每次都会new一个StringBuilder调用invokevirtual StringBuilder.append调用invokevirtual StringBuilder.toString生成新String而手动使用StringBuilder只new一次循环中只调用append3.4 Java 9 的invokedynamic优化Java 9 引入了更高效的字符串拼接方式通过invokedynamic调用StringConcatFactory// Java 9 的编译结果非循环StringsHelloname!;// 底层调用 StringConcatFactory.makeConcatWithConstants但这个优化同样只适用于单个表达式循环内依然每次都会触发拼接逻辑。四、正确写法4.1 循环拼接手动使用StringBuilder// ✅ 正确写法StringBuildersbnewStringBuilder();for(Orderorder:orders){sb.append(order.getId()).append(:).append(order.getStatus()).append(,);}Stringresultsb.toString();4.2 预估容量减少扩容开销// ✅ 如果大致知道最终长度指定初始容量StringBuildersbnewStringBuilder(orders.size()*32);for(Orderorder:orders){sb.append(order.getId()).append(:).append(order.getStatus()).append(\n);}StringBuilder内部是char[]默认容量 16超出后扩容为旧容量 * 2 2涉及数组复制影响性能。4.3 非循环场景直接用让编译器优化// ✅ 完全没问题编译器会优化Stringmessage用户user.getName()年龄user.getAge();log.info(message);不需要手动改成StringBuilder反而降低代码可读性。4.4 复杂拼接用String.format或文本块Java 15// ✅ String.format格式清晰StringsqlString.format(SELECT * FROM %s WHERE id %d,tableName,id);// ✅ Java 15 文本块适合 SQL / JSON / HTMLStringjson { name: %s, age: %d } .formatted(name,age);五、最佳实践✅ 字符串拼接选择指南场景推荐方式原因单行简单拼接String 编译器优化代码清晰循环内拼接StringBuilder避免重复创建对象复杂格式化String.format可读性最好SQL / JSON / HTML文本块Java 15多行字符串支持超高性能场景StringBuilder 预分配容量减少扩容 3 个常见误区误区 1「StringBuffer比StringBuilder慢永远不用」→ 正确StringBuffer是线程安全的多线程共享拼接对象时用它。误区 2「所有拼接都要改成StringBuilder」→ 正确只有循环内的拼接需要改单行拼接编译器已经优化了。误区 3「StringBuilder用完要调用toString()再清空」→ 正确toString()不影响原StringBuilder可继续append。️ IDEA inspections开启以下检查让 IDE 帮你找问题 on different lines→ 警告多行用拼接StringBuilder.append()can be replaced withStringconcatenation→ 提示单行可简化String concatenation in loop→错误循环中字符串拼接最重要六、小结单行/单个表达式内的拼接编译器自动优化成StringBuilder性能无问题循环内的拼接每次循环创建新StringBuilder 新String性能极差正确做法循环外创建StringBuilder循环内只调用append容量预分配大致知道最终长度时指定初始容量减少扩容Java 9引入invokedynamic优化但循环场景依然需要手动StringBuilder下一篇预告重写 equals 不重写 hashCode 会怎样—— HashMap 里的对象丢了竟然是因为这个原因。