JDK 21 字符串拼接最佳实践:场景化选择最优方案
在 Java 开发中,字符串拼接是最基础也最高频的操作之一。从最初的+号拼接,到StringBuilder、StringBuffer,再到 JDK 15 引入的文本块(Text Blocks)和String.formatted()方法,Java 对字符串拼接的支持一直在迭代优化。
到了JDK 21,字符串拼接早已没有 “唯一最优解”,而是需要根据编译期 / 运行时、单线程 / 多线程、简单拼接 / 格式化模板等不同场景选择对应的方式。本文将结合 JDK 21 的特性,为你梳理不同场景下的字符串拼接最优方案,附详细代码示例和原理解析,帮你写出高效又优雅的代码。
一、先明确核心结论
为了方便你快速查阅,先给出不同场景的最优选择总结,后续再逐个场景展开详解:
| 应用场景 | 推荐拼接方式 | 核心优势 |
|---|---|---|
| 编译期静态拼接(无运行时变量) | +号直接拼接 |
语法简洁,编译器自动优化性能 |
| 单线程运行时动态拼接(如循环) | StringBuilder |
无同步开销,性能最优 |
| 格式化模板 / 多行字符串拼接 | String.formatted() + 文本块 |
可读性拉满,适配复杂模板 |
| 集合 / 数组元素分隔拼接 | String.join()/Stream 流 |
无需手动循环,代码简洁 |
| 多线程并发拼接 | StringBuffer |
线程安全,支持并发操作 |
二、编译期静态拼接:直接用+号就够了
很多开发者会陷入 “+号拼接一定低效” 的误区,但这个结论仅适用于运行时动态拼接。对于编译期就能确定内容的静态拼接(比如常量、字面量拼接),+号是 JDK 21 中最推荐的方式。
原理:编译器的自动优化
JDK 9 之后,编译器会将静态拼接的+号优化为invokedynamic指令,调用StringConcatFactory直接生成最终字符串,完全避免创建中间String对象,性能与StringBuilder持平,且代码更简洁。
代码示例
public class StaticConcatDemo {
// 编译期常量
private static final String JDK_VERSION = "JDK 21";
private static final String OPERATION = "字符串拼接";
private static final String SCENE = "静态拼接";
public static void main(String[] args) {
// 编译期即可确定的静态拼接,直接用+号
String result = JDK_VERSION + "的" + SCENE + ":" + OPERATION + "最优解";
System.out.println(result); // 输出:JDK 21的静态拼接:字符串拼接最优解
}
}
三、单线程运行时动态拼接:优先用 StringBuilder
当拼接的内容包含运行时变量(比如循环中的动态数据、用户输入的参数),且处于单线程环境时,StringBuilder是 JDK 21 中的性能最优选择。
为什么不选其他?
StringBuffer:因加了synchronized同步锁,会带来约 30% 的性能损耗,仅适用于多线程场景。- 直接用
+号:运行时会频繁创建String和StringBuilder临时对象,循环中使用会导致性能急剧下降。
代码示例(循环拼接)
import java.util.ArrayList;
import java.util.List;
public class StringBuilderDemo {
public static void main(String[] args) {
// 运行时动态生成的列表数据
List<String> fruitList = new ArrayList<>();
fruitList.add("苹果");
fruitList.add("香蕉");
fruitList.add("橙子");
fruitList.add("葡萄");
// 单线程动态拼接,使用StringBuilder
StringBuilder sb = new StringBuilder();
for (String fruit : fruitList) {
sb.append(fruit).append(" | ");
}
// 去除最后一个多余的分隔符
if (sb.length() > 0) {
sb.delete(sb.length() - 3, sb.length());
}
String result = sb.toString();
System.out.println(result); // 输出:苹果 | 香蕉 | 橙子 | 葡萄
}
}
JDK 21 小优化:StringBuilder的append方法对字符序列的处理做了细微的性能优化,核心使用方式与此前版本一致,但在大数据量拼接时效率略有提升。
四、格式化模板拼接:formatted () + 文本块,优雅到极致
在需要占位符格式化(如%s、%d)或多行字符串拼接(如 JSON、SQL、HTML 模板)的场景,JDK 21 推荐结合使用String.formatted()(JDK 15 引入)和文本块(Text Blocks)(JDK 15 正式特性),这是兼顾可读性和简洁性的最优解。
4.1 单行格式化:String.formatted ()
String.formatted()是String.format()的升级版,语法更简洁(直接通过字符串实例调用),性能与String.format()持平,代码可读性更高。
代码示例
public class FormattedSingleLineDemo {
public static void main(String[] args) {
String framework = "Redisson";
int version = 3;
double performance = 200.8;
// 传统写法:String.format()
String oldFormat = String.format("分布式锁框架:%s,版本:%d,性能提升:%.1f%%", framework, version, performance);
// JDK 15+推荐:String.formatted()
String newFormat = "分布式锁框架:%s,版本:%d,性能提升:%.1f%%".formatted(framework, version, performance);
System.out.println(oldFormat); // 输出:分布式锁框架:Redisson,版本:3,性能提升:200.8%
System.out.println(newFormat); // 与上一行结果一致
}
}
4.2 多行模板:文本块 + formatted ()
文本块用"""包裹,无需手动拼接换行符\n和转义引号,结合formatted()可轻松处理复杂的多行模板,这是 JDK 21 中处理 JSON、SQL 模板的最佳方式。
代码示例(JSON 模板)
public class TextBlocksDemo {
public static void main(String[] args) {
// 运行时动态参数
String lockKey = "distributed:lock:order";
long leaseTime = 30;
boolean isLockSuccess = true;
String clientId = "redisson-client-12345";
// 文本块定义多行JSON模板 + formatted()填充参数
String jsonTemplate = """
{
"lockConfig": {
"lockKey": "%s",
"leaseTime": %d,
"isLockSuccess": %b,
"clientId": "%s",
"description": "JDK 21文本块拼接的Redisson锁配置"
}
}
""".formatted(lockKey, leaseTime, isLockSuccess, clientId);
System.out.println(jsonTemplate);
}
}
输出结果
{
"lockConfig": {
"lockKey": "distributed:lock:order",
"leaseTime": 30,
"isLockSuccess": true,
"clientId": "redisson-client-12345",
"description": "JDK 21文本块拼接的Redisson锁配置"
}
}
五、集合 / 数组元素拼接:String.join (),告别手动循环
当需要将集合(List/Set) 或数组的元素按指定分隔符拼接时,JDK 提供的String.join()是最优选择,无需手动写循环,代码简洁且性能优异。
5.1 基础用法:直接拼接集合 / 数组
import java.util.Arrays;
import java.util.List;
public class StringJoinDemo {
public static void main(String[] args) {
// 1. 集合元素拼接
List<String> lockTypes = List.of("RLock", "RFairLock", "RReadWriteLock", "RedissonRedLock");
String lockTypeStr = String.join(" → ", lockTypes);
System.out.println("Redisson锁类型:" + lockTypeStr);
// 输出:Redisson锁类型:RLock → RFairLock → RReadWriteLock → RedissonRedLock
// 2. 数组元素拼接
String[] redisModes = {
"单机模式", "集群模式", "哨兵模式", "主从模式"};
String redisModeStr = String.join(" | ", redisModes);
System.out.println("Redis部署模式:" + redisModeStr);
// 输出:Redis部署模式:单机模式 | 集群模式 | 哨兵模式 | 主从模式
}
}
5.2 进阶用法:Stream 流 + Collectors.joining ()
若需要对集合元素先做过滤、转换再拼接,可结合 Stream 流的Collectors.joining(),实现更灵活的拼接逻辑。
import java.util.List;
import java.util.stream.Collectors;
public class StreamJoinDemo {
public static void main(String[] args) {
List<Integer> expireTimes = List.of(10, 20, 30, 40, 50);
// Stream流转换:过滤偶数 → 转为字符串 → 拼接
String timeStr = expireTimes.stream()
.filter(t -> t % 2 == 0) // 过滤偶数过期时间
.map(String::valueOf) // 转为字符串
.collect(Collectors.joining("s, ", "Redisson锁过期时间:", "s")); // 拼接前缀、分隔符、后缀
System.out.println(timeStr);
// 输出:Redisson锁过期时间:10s, 20s, 30s, 40s, 50s
}
}
六、多线程并发拼接:StringBuffer,线程安全的选择
仅当多线程同时操作同一个字符序列时(如多线程日志拼接、共享字符串缓存),才需要使用StringBuffer—— 它是 JDK 中唯一支持并发安全的可变字符序列,通过synchronized关键字保证多线程下的操作原子性。
代码示例
public class StringBufferDemo {
// 多线程共享的StringBuffer对象
private static final StringBuffer SHARED_BUFFER = new StringBuffer();
public static void main(String[] args) throws InterruptedException {
// 创建3个线程并发拼接字符串
Thread t1 = new Thread(() -> SHARED_BUFFER.append("线程1执行任务").append(" | "));
Thread t2 = new Thread(() -> SHARED_BUFFER.append("线程2执行任务").append(" | "));
Thread t3 = new Thread(() -> SHARED_BUFFER.append("线程3执行任务").append(" | "));
// 启动线程并等待执行完成
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
// 去除最后一个多余的分隔符
if (SHARED_BUFFER.length() > 0) {
SHARED_BUFFER.delete(SHARED_BUFFER.length() - 3, SHARED_BUFFER.length());
}
System.out.println(SHARED_BUFFER);
// 输出(线程执行顺序可能不同):线程1执行任务 | 线程2执行任务 | 线程3执行任务
}
}
重要提醒:若无需多线程安全,切勿使用StringBuffer—— 其同步锁会带来不必要的性能开销,优先选择StringBuilder。
七、常见误区澄清
误区 1:
+号拼接一定低效。正解:仅运行时动态拼接(如循环中)低效,编译期静态拼接的
+号会被编译器优化,性能与StringBuilder持平。误区 2:文本块只是简化了换行符,没什么实际作用。
正解:文本块不仅省去了
\n和转义引号,还能保持字符串的原始格式,大幅提升 JSON、SQL 等模板的可读性和可维护性。误区 3:
String.formatted()比String.format()性能更好。正解:两者性能基本一致,
formatted()的优势是语法更简洁,属于 “语法糖” 优化。
八、总结
JDK 21 的字符串拼接不再有 “一刀切” 的方案,而是场景化的最优选择:
- 静态拼接用
+号,享受编译器的自动优化; - 单线程动态拼接用
StringBuilder,追求极致性能; - 格式化模板用
formatted()+ 文本块,兼顾优雅与可读性; - 集合 / 数组拼接用
String.join()或 Stream 流,告别冗余循环; - 多线程拼接用
StringBuffer,保证线程安全。
遵循以上原则,在 JDK 21 中写出的字符串拼接代码,既能兼顾性能,又能让代码更优雅、更易维护。