一个被语法糖耽误的拼接需求
上周我要拼一段带变量的 SQL 调试日志,里面混入三个变量。老写法要么是加号拼接,要么是 String.format,前者冗长后者位置参数容易错位。听同事说 JDK 21 的预览特性 String Templates 把这件事做漂亮了,我拉了 early-access 构建试了试,结论是:方向真不错,但在生产落地前还有几处要想清楚。
三种写法摆在一起
同样的需求,三种写法对比一下,差异立刻显现:
String user = "zixin";
int age = 29;
String city = "Shanghai";
// 1. 加号拼接:变量一多就满屏加号,括号容易漏
String s1 = "用户 " + user + " 年龄 " + age + " 城市 " + city;
// 2. String.format:位置参数,顺序错就乱套,还丢类型信息
String s2 = String.format("用户 %s 年龄 %d 城市 %s", user, age, city);
// 3. String Templates(STR 处理器)
String s3 = STR."用户 \{user} 年龄 \{age} 城市 \{city}";
第三种最直观。\{ } 里直接塞表达式,编译期由 STR 处理器展开。我跑了 100 万次拼接做基准:三者耗时分别是 89ms、142ms、91ms。STR 和加号拼接几乎打平,比 format 快不少,因为 format 每次都要解析占位符字符串。性能不是重点,重点是可读性。
可读性才是真正的收益
真实业务里经常是十几个字段拼一条消息,加号写法要数清楚哪段接哪段,format 写法要靠 %s 和下方的参数列表一一对应,少传一个就 IndexOutOfBoundsException,多传一个没人发现。STR 把变量直接嵌在它该在的位置,眼到手到。我把团队一段 40 行的订单日志拼装改成 STR,缩到 12 行,新人看一眼就懂。
| 维度 | 加号拼接 | String.format | String Templates |
|---|---|---|---|
| 可读性 | 差(变量散落) | 中(位置易错) | 好(就近嵌入) |
| 类型安全 | 编译期 | 运行期才报错 | 编译期 |
| 百万次耗时 | 89ms | 142ms | 91ms |
| 占位错位风险 | 无 | 高 | 无 |
预览特性怎么跑起来
它还是预览,得显式开启:
javac --release 21 --enable-preview Demo.java
java --enable-preview Demo
用 Maven 的话,compiler 插件要加 enable-preview:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>21</release>
<compilerArgs>--enable-preview</compilerArgs>
</configuration>
</plugin>
注意 surefire 跑测试也要加 --enable-preview,否则测试编译不过,这个坑我踩了半小时。另外构建产物的 class 文件带预览标记,运行时必须用同版本 JDK 加 --enable-preview,否则报 UnsupportedClassVersionError。
它能做的远不止拼接
STR 只是内置处理器之一。FMT 可以做本地化格式化,比如按区域显示数字和日期:
String s = FMT."金额 \{amount,number,currency} 时间 \{now,time,short}";
更有用的是 RAW 处理器,它把模板拆成片段和值两部分,方便做 SQL 预编译或 HTML 转义:
StringTemplate st = RAW."SELECT * FROM t WHERE name = '\{name}'";
// st.fragments() 拿到 SQL 骨架,st.values() 拿到参数,交给 PreparedStatement
把骨架和值分开,注入就无从谈起。这个思路比手动拼字符串安全多了,我们后来在内部工具里用 RAW 统一做了一层防注入。还有一点巧妙:你可以自己写 TemplateProcessor,比如把模板渲染成 JSON 或 Markdown,扩展性比 format 强太多。
什么时候我不建议用
一是生产环境,预览特性的语法和处理器 API 在转正前可能变动,升级 JDK 时要重测;二是超高频、极致性能的循环里,虽然它和加号差不多快,但多一层处理器调用,能省则省。日常业务代码、日志、SQL 拼装,它是把好刀。
和文本块(Text Blocks)配合更香
JDK 15 转正的多行文本块解决的是"换行和缩进",String Templates 解决的是"插值"。两者可以叠加,写一段带变量的 JSON 或 HTML 就舒服了:
String body = STR."""
{
"user": "\{user}",
"age": \{age},
"city": "\{city}"
}""";
以前这种 JSON 要么拼字符串要么引 Jackson,现在模板直接出。我们内部有个生成测试数据的小工具,原来 60 行 StringBuilder,改成 STR 加文本块后 20 行,而且不会漏掉引号转义。
一个我踩过的小坑
\{ } 里的表达式如果是方法调用,要注意它会每次求值。我一开始在模板里写了 STR."共 \{list.size()} 条",循环里反复拼接,结果 list.size() 被调了好几次。虽然这里没副作用不影响结果,但如果表达式有副作用就会重复执行。约定上模板里只放纯读取表达式,副作用留在外面算好再传进来。
小结
字符串模板目前还是预览特性,生产别急着上,局部工具脚本可以试。但方向我很喜欢:把"模板加处理器"解耦,拼接只是最朴素的一种。等它正式转正,日志、SQL、JSON 拼装都能更干净,也少很多注入隐患。我已经在个人项目里全面用上了,就等它在主项目里转正。如果你也在用 JDK 21,建议先在一个非核心脚本里试水,感受一下它和加号、format 在可读性和可维护性上的差距。