Java 23 新特性全景:String Templates、Unnamed Patterns的工程价值
Java 23 新特性全景:String Templates、Unnamed Patterns的工程价值
适读人群:Java工程师全体 | 阅读时长:约15分钟 | 技术栈:Java 21-23、Pattern Matching、Records
开篇故事
每次Java发布新版本,我都有一个固定动作:先看JEP列表,再实际写代码试一遍,然后问自己一个问题:这些特性我什么时候会在生产代码里用到?
Java 21/22/23这几个版本,Loom项目(虚拟线程、结构化并发)和Amber项目(模式匹配、记录类)同时落地,让我觉得Java语言终于在"现代化"这条路上迈出了实质性的一步,而不只是修修补补。
今天这篇文章,我把Java 21-23的主要新特性按实用性排个序,重点讲那些真正能改变你写代码方式的特性,顺带说说那些"看起来不错但实际用途有限"的特性。不全面铺开,只讲工程价值。
一、核心问题:Java语言现代化的方向
Java新特性的演进有几条主线:
这几年Java明显在学习Kotlin和Scala的优点,同时保持向后兼容性。我认为这个方向是对的——Java不应该变成另一门语言,但应该减少那些让开发者痛苦的冗余语法。
二、特性深度解析
2.1 String Templates(JEP 430,Java 21预览)
这个特性我等了很久。在Java 21之前,字符串拼接一直是个痛点,尤其是构建SQL、JSON、XML时:
// 旧方式:字符串拼接,可读性差
String sql = "SELECT * FROM users WHERE id = " + userId +
" AND status = '" + status + "' AND name LIKE '%" + name + "%'";
// String.format:好一点,但对齐困难
String sql = String.format(
"SELECT * FROM users WHERE id = %d AND status = '%s' AND name LIKE '%%%s%%'",
userId, status, name);String Templates提供了更优雅的方案:
// String Templates:简洁且类型安全
// 注意:Java 23中String Templates仍是预览特性,最终版本API可能调整
import static java.lang.StringTemplate.STR;
String sql = STR."SELECT * FROM users WHERE id = \{userId} AND status = '\{status}'";
// 多行文本块 + String Templates
String json = STR."""
{
"id": \{user.getId()},
"name": "\{user.getName()}",
"email": "\{user.getEmail()}"
}
""";但String Templates真正强大的地方是自定义处理器:
// 内置处理器
STR."Hello \{name}" // 简单字符串插值
FMT."%.2f\{price}" // 格式化插值
RAW."Hello \{name}" // 返回StringTemplate对象(延迟处理)
// 自定义SQL安全处理器 - 防SQL注入
StringTemplate.Processor<PreparedStatement, SQLException> SQL =
template -> {
StringBuilder sql = new StringBuilder();
List<Object> params = new ArrayList<>();
for (int i = 0; i < template.fragments().size(); i++) {
sql.append(template.fragments().get(i));
if (i < template.values().size()) {
sql.append("?"); // 用占位符代替直接插值
params.add(template.values().get(i));
}
}
PreparedStatement stmt = connection.prepareStatement(sql.toString());
for (int i = 0; i < params.size(); i++) {
stmt.setObject(i + 1, params.get(i));
}
return stmt;
};
// 使用自定义处理器,永远不会有SQL注入风险!
PreparedStatement stmt = SQL."SELECT * FROM users WHERE id = \{userId}";这个自定义处理器机制,让字符串模板在保持可读性的同时,还能做类型安全、安全编码等处理。这才是String Templates的核心价值,不只是语法糖。
2.2 Pattern Matching全景:从instanceof到switch
Pattern Matching是Java近几个版本变化最大的领域,从Java 16到23经历了多个阶段:
// Java 16: instanceof模式匹配(正式)
// 旧方式
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.length());
}
// 新方式
if (obj instanceof String s) {
System.out.println(s.length()); // 直接用s,无需强转
}
// Java 21: switch模式匹配(正式)
String describe(Object obj) {
return switch (obj) {
case Integer i when i > 0 -> "正整数: " + i;
case Integer i -> "非正整数: " + i;
case String s when s.isEmpty() -> "空字符串";
case String s -> "字符串: " + s;
case null -> "null";
default -> "其他类型: " + obj.getClass().getSimpleName();
};
}2.3 Unnamed Patterns(JEP 443,Java 21预览)
这个特性比较小,但在实际代码中很有用:
// 当你只关心类型,不关心变量名时
// 旧方式:必须给变量命名,但不用它
if (obj instanceof String s) {
return "是字符串"; // s根本没被用到
}
// 新方式:用 _ 表示不关心变量
if (obj instanceof String _) {
return "是字符串";
}
// 在switch中更实用
switch (event) {
case MouseEvent(int x, int y, _, _) -> handleMouseAt(x, y); // 只关心坐标
case KeyEvent(char key, _) -> handleKey(key); // 只关心按键
case _ -> {} // 忽略其他所有事件
}2.4 Record Patterns(JEP 440,Java 21正式)
Records和Pattern Matching的结合,让数据解构变得优雅:
// 定义Record
record Point(int x, int y) {}
record Rectangle(Point topLeft, Point bottomRight) {}
// Record Pattern解构
Object shape = new Rectangle(new Point(0, 0), new Point(10, 10));
// 嵌套解构
if (shape instanceof Rectangle(Point(int x1, int y1), Point(int x2, int y2))) {
int width = x2 - x1;
int height = y2 - y1;
System.out.printf("矩形: 宽%d, 高%d%n", width, height);
}
// 在switch中解构
String describe(Object shape) {
return switch (shape) {
case Rectangle(Point(int x1, int y1), Point(int x2, int y2)) ->
STR."矩形 (\{x1},\{y1}) 到 (\{x2},\{y2})";
case Point(int x, int y) ->
STR."点 (\{x},\{y})";
default -> "未知形状";
};
}2.5 Sealed Classes与Pattern Matching的完美配合
// 密封类:明确定义所有子类型
sealed interface Result<T> permits Success, Failure, Loading {}
record Success<T>(T value) implements Result<T> {}
record Failure<T>(String error, Throwable cause) implements Result<T> {}
record Loading<T>() implements Result<T> {}
// 处理Result时,编译器知道所有可能的类型
String handle(Result<User> result) {
return switch (result) {
case Success<User>(User user) -> "用户: " + user.getName();
case Failure<User>(String error, _) -> "错误: " + error;
case Loading<User> _ -> "加载中...";
// 编译器会检查是否覆盖了所有情况,不需要default!
};
}三、完整代码实现:用新特性重构真实业务代码
3.1 API响应处理的演进
// 旧方式:大量的null检查和类型转换
public UserVO processApiResponse(Map<String, Object> response) {
Object data = response.get("data");
if (data == null) {
return UserVO.empty();
}
if (!(data instanceof Map)) {
throw new ParseException("data字段格式错误");
}
Map<String, Object> dataMap = (Map<String, Object>) data;
Object userId = dataMap.get("id");
if (!(userId instanceof Number)) {
throw new ParseException("id字段格式错误");
}
return new UserVO(((Number) userId).longValue(), (String) dataMap.get("name"));
}
// 新方式:Pattern Matching + Records
public UserVO processApiResponseNew(Map<String, Object> response) {
return switch (response.get("data")) {
case Map<?, ?> data when data.get("id") instanceof Number id ->
new UserVO(id.longValue(), (String) data.get("name"));
case null -> UserVO.empty();
default -> throw new ParseException("data字段格式错误");
};
}3.2 领域事件处理
// 定义密封的领域事件体系
sealed interface OrderEvent permits
OrderCreated, OrderPaid, OrderShipped, OrderCancelled {}
record OrderCreated(Long orderId, Long userId, BigDecimal amount) implements OrderEvent {}
record OrderPaid(Long orderId, String paymentId, Instant paidAt) implements OrderEvent {}
record OrderShipped(Long orderId, String trackingNo, String carrier) implements OrderEvent {}
record OrderCancelled(Long orderId, String reason, Instant cancelledAt) implements OrderEvent {}
// 事件处理:编译器强制处理所有事件类型
@Service
public class OrderEventHandler {
public void handle(OrderEvent event) {
switch (event) {
case OrderCreated(Long orderId, Long userId, BigDecimal amount) -> {
log.info("订单创建: orderId={}, userId={}, amount={}", orderId, userId, amount);
notifyUser(userId, STR."您的订单\{orderId}已创建,金额\{amount}元");
reserveInventory(orderId);
}
case OrderPaid(Long orderId, String paymentId, _) -> {
log.info("订单支付: orderId={}, paymentId={}", orderId, paymentId);
triggerFulfillment(orderId);
}
case OrderShipped(Long orderId, String trackingNo, String carrier) -> {
updateTrackingInfo(orderId, trackingNo, carrier);
}
case OrderCancelled(Long orderId, String reason, _) -> {
log.info("订单取消: orderId={}, reason={}", orderId, reason);
releaseInventory(orderId);
processRefund(orderId);
}
// 无需default!如果新增事件类型,这里编译就会报错,强制处理
}
}
}3.3 构建类型安全的查询DSL
// 结合新特性构建类型安全的查询条件
sealed interface Condition permits
AndCondition, OrCondition, EqualCondition, LikeCondition, RangeCondition {}
record EqualCondition(String field, Object value) implements Condition {}
record LikeCondition(String field, String pattern) implements Condition {}
record RangeCondition(String field, Comparable<?> min, Comparable<?> max) implements Condition {}
record AndCondition(List<Condition> conditions) implements Condition {}
record OrCondition(List<Condition> conditions) implements Condition {}
// 将Condition转换为SQL
public class ConditionRenderer {
public String render(Condition condition) {
return switch (condition) {
case EqualCondition(String field, Object value) ->
STR."\{field} = '\{value}'";
case LikeCondition(String field, String pattern) ->
STR."\{field} LIKE '\{pattern}'";
case RangeCondition(String field, var min, var max) ->
STR."\{field} BETWEEN '\{min}' AND '\{max}'";
case AndCondition(List<Condition> conditions) ->
conditions.stream()
.map(this::render)
.collect(Collectors.joining(" AND ", "(", ")"));
case OrCondition(List<Condition> conditions) ->
conditions.stream()
.map(this::render)
.collect(Collectors.joining(" OR ", "(", ")"));
};
}
}四、工程价值评估
4.1 特性实用性排名(我的主观评价)
| 特性 | Java版本 | 实用性 | 我的评价 |
|---|---|---|---|
| Pattern Matching for switch | 21 | ★★★★★ | 必学,改变处理多态的方式 |
| Records | 16+ | ★★★★★ | 已在项目中大量使用 |
| Sealed Classes | 17+ | ★★★★☆ | 配合Pattern Matching极好用 |
| Text Blocks | 15+ | ★★★★☆ | SQL/JSON字符串必用 |
| Virtual Threads | 21 | ★★★★★ | IO密集型服务必升 |
| Record Patterns | 21 | ★★★★☆ | 减少解构代码 |
| String Templates | 预览 | ★★★★☆ | 等正式版再用 |
| Unnamed Patterns | 预览 | ★★★☆☆ | 有用但不紧迫 |
| Structured Concurrency | 预览 | ★★★★☆ | 等Java 25 LTS |
五、踩坑实录
坑一:Pattern Matching和null的关系
// 陷阱:null不匹配任何类型模式
Object obj = null;
// 这里不会进入任何case,直接抛NullPointerException!
switch (obj) {
case String s -> System.out.println(s);
case Integer i -> System.out.println(i);
// 没有null case,直接NPE
}
// 正确:必须显式处理null
switch (obj) {
case null -> System.out.println("null值");
case String s -> System.out.println(s);
case Integer i -> System.out.println(i);
default -> System.out.println("其他");
}坑二:when守卫的变量作用域
// 错误:when中用到的变量必须在当前模式中声明
switch (obj) {
// 编译错误:y在这个case分支的when子句中不可用
case Point(int x, int y) when x > 10 -> "x大于10";
// 正确用法
case Point(int x, int y) when x > 10 && y > 10 -> "都大于10";
}坑三:Record Pattern的深层解构顺序
嵌套Record Pattern的解构,是在运行时按顺序执行的,如果内层解构失败,会跳过整个case,不会进入。这个行为在复杂嵌套时容易产生意外。
坑四:预览特性的版本兼容性
String Templates在Java 21到23之间API发生了调整,21版本的STR处理器在某些情况下和23版本行为略有不同。在正式特性发布之前,谨慎在生产环境使用。
六、总结与个人判断
Java 21-23的新特性,从工程角度来说,最值钱的是三件事:
第一,Pattern Matching(包括switch模式匹配和Record Patterns)真正让Java代码在处理复杂类型层级时变得优雅,减少了大量的instanceof判断和强转代码。
第二,Sealed Classes配合Pattern Matching,让"代数数据类型"在Java里成为现实,处理有限状态的业务逻辑时,编译器能帮你检查是否遗漏了情况。
第三,Records在DTO、值对象、事件等场景下,极大减少了样板代码。
String Templates我认为等Java 25 LTS出来再看。预览特性风险不低,而且大多数团队用MessageFormat或者StringBuilder也凑合。
我建议的升级路径:先升到Java 21(LTS),充分利用虚拟线程和已正式的Pattern Matching特性,这个收益最高、风险最低。
