
Java 字符串拼接性能实测:、StringBuilder、String.join 到底什么时候用哪个面试常问「字符串拼接为什么不能用 」,但真到写代码时,你可能又拿不准:一个简单的Hello, name到底有没有性能问题?循环里拼 SQL 该用什么?把一个 list 拼成逗号分隔的字符串,手写 StringBuilder 还是有更优雅的写法?这篇用反编译和基准数字把这几个场景讲清楚,让你以后拼字符串不再凭感觉。先破除一个误解:单次 拼接没问题很多人听说「 拼接慢」就矫枉过正,连Hello, name都要改成 StringBuilder,这完全没必要。看这段代码编译后是什么样:Stringgreet(Stringname){returnHello, name!;}用javap -c反编译(JDK 9 及以后),你会看到编译器把它优化成了invokedynamic调用StringConcatFactory——单行的 拼接,编译器已经帮你优化了,性能和手写没差。所以:非循环、拼接次数固定的场景,大胆用 ,可读性最好。真正的性能杀手:循环里用 问题出在循环。看这段「朴素写法」:// 反面教材:循环里用 拼接StringbuildCsv(ListStringitems){Stringresult;for(Stringitem:items){resultitem,;// 每次循环都新建一个 String 对象}returnresult;}为什么慢?因为 String 是不可变的。result item实际上是每次循环都 new 一个 StringBuilder、拼接、再 toString 成新 String。编译器没法把这个优化跨循环合并,于是拼 N 个元素就产生了 N 个临时 StringBuilder 和 N 个中间 String。拼 1 万个元素,这段代码的时间复杂度是 O(n²)——因为每次都要把已有的 result 整个复制一遍。实测拼 5 万个短字符串,这种写法要几百毫秒到几秒,而下面的正确写法只要几毫秒。循环拼接的正解:复用一个 StringBuilder把 StringBuilder 提到循环外面,全程只用这一个:// 正确写法:一个 StringBuilder 从头用到尾StringbuildCsv(ListStringitems){StringBuildersbnewStringBuilder();for(Stringitem:items){sb.append(item).append(,);// append 直接改内部 char 数组,不新建对象}returnsb.toString();// 只在最后生成一次 String}关键差异:StringBuilder 内部维护一个可变的字符数组,append是往数组尾部追加,容量不够时才扩容(每次翻倍),整体是 O(n)。如果你能预估最终长度,构造时传入初始容量,连扩容都能省掉:// 预分配容量,避免中途多次扩容拷贝StringBuildersbnewStringBuilder(items.size()*16);一个常见追问:StringBuilder 和 StringBuffer 怎么选?单线程无脑用 StringBuilder。StringBuffer 的每个方法都加了 synchronized,单线程下白白付出锁开销,而字符串拼接几乎永远是局部变量、不跨线程。拼接集合?别手写循环,用 String.join如果你的需求就是「把一个集合用分隔符连起来」——这是最高频的场景——根本不用手写 StringBuilder 循环。JDK 8 的String.join一行搞定,还自动处理了「最后一个元素不加分隔符」这个手写时最容易错的边界:ListStringnamesList.of(alice,bob,carol);// 一行拼成 alice,bob,carol,末尾没有多余逗号StringcsvString.join(,,names);需要更复杂的拼接(带前缀后缀、先做映射转换),用Collectors.joining:importstaticjava.util.stream.Collectors.joining;ListIntegeridsList.of(1,2,3);// 输出 (1, 2, 3):分隔符 , ,前缀 (,后缀 )Stringresultids.stream().map(String::valueOf).collect(joining(, ,(,)));String.join和Collectors.joining底层都用了StringJoiner,性能和手写 StringBuilder 几乎一致,但代码短一半、还不会漏掉边界。能用 join 就别手写循环。一张选型速查表把四种场景对应到写法,记住这个就够了:单次、固定次数拼接(如Hello, name):直接用,编译器已优化,别过度设计。循环内累加拼接:循环外建一个StringBuilder,循环内append,最后toString。能预估长度就传初始容量。把集合连成分隔字符串:String.join(分隔符, 集合),一行搞定边界。拼接前需要映射/加前后缀:Stream Collectors.joining(分隔符, 前缀, 后缀)。还有个反直觉的坑:不要用拼 StringBuilder 的结果,比如sb.append(a x b)。里层的a x b会先自己 new 一个临时 StringBuilder,失去意义。应该拆成sb.append(a).append(x).append(b),链式调用一个对象到底。小结单次拼接用 ,JDK 9 编译器会优化成StringConcatFactory,和手写等价,可读性最优。循环拼接的性能陷阱在于每次都新建对象,是 O(n²);复用一个StringBuilder降到 O(n),预分配容量还能免扩容。单线程一律用 StringBuilder,别用带锁的 StringBuffer。拼集合优先String.join/Collectors.joining,自动处理分隔符边界,比手写更短更稳。一句话记忆:固定次数用 ,循环累加用 StringBuilder,连集合用 join——凭场景选,别一刀切。