Java 模式匹配(Pattern Matching)自 Java 16 以预览特性登场、到 Java 21 正式转正,彻底改变了我们处理类型判断与数据解构的方式。过去要写一堆 instanceof 强转、switch 嵌套判断的代码,现在可以用更紧凑、更安全、可读性更高的语法来表达。本文带你从 instanceof 模式匹配、switch 类型模式、守卫模式,一路讲到 Record 解构模式,并给出一个真实的老代码重构案例,帮你把这套现代 Java 特性真正用进项目里。
一、为什么需要模式匹配
在模式匹配出现之前,Java 做”先判断类型、再取出数据”是一件很啰嗦的事:instanceof 判断之后必须再强转一次,变量名重复、缩进层层嵌套;switch 只能匹配常量,无法按类型分发;处理密封类(sealed)时,虽然语义上要求穷举,但编译器并不会替你检查是否漏了某个分支。这些痛点让大量样板代码挤占了真正表达业务逻辑的篇幅,也埋下了强转抛 ClassCastException 的隐患。
二、instanceof 模式匹配(Java 16 正式)
2.1 旧写法 vs 新写法
Java 16 起,instanceof 支持在判断的同时直接声明一个类型正确的模式变量,省去显式强转:
// 旧写法:判断后再强转
Object obj = getResult();
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.length());
}
// 新写法:模式变量 s 在为真时已就位
Object obj = getResult();
if (obj instanceof String s) {
System.out.println(s.length()); // 无需强转,s 已是 String
}
2.2 在返回值与方法参数中应用
模式变量不仅能用在 if 里,也能链式地出现在方法返回中,配合 List<?> 这样的通配类型同样适用:
String describe(Object o) {
if (o instanceof Integer n) return "int:" + n;
if (o instanceof String s) return "str:" + s;
if (o instanceof List<?> list) return "list size=" + list.size();
return "unknown";
}
2.3 模式变量的作用域边界
一个容易忽略的细节:模式变量只在”条件为真”的分支内可见,不能跨到对立分支复用。下面这段代码在 else 里引用 s 会直接编译失败,因为编译器无法保证进入 else 时 obj 仍是 String——这恰恰是它比手写强转更安全的地方:
if (obj instanceof String s) {
System.out.println(s.length());
} else {
// System.out.println(s); // 编译错误:s 不在作用域内
}
三、switch 类型模式(Java 17 预览 → 21 正式)
3.1 类型模式 switch
Java 21 把”按类型分发”正式带进了 switch 表达式,分支里直接拿到对应类型的变量:
String typeName(Object o) {
return switch (o) {
case Integer i -> "Integer:" + i;
case String s -> "String:" + s.length();
case Double d -> "Double:" + d;
default -> "other";
};
}
3.2 守卫模式 when(Java 21+)
当同一个类型还需要按值域细分时,用 when 守卫做二次约束,避免再写一层 if:
double discount(Object o) {
return switch (o) {
case Integer i when i > 100 -> 0.2;
case Integer i -> 0.1;
case String s when !s.isBlank()-> 0.05;
case String s -> 0.0;
default -> 0.0;
};
}
3.3 与密封类 sealed 配合
模式匹配真正的威力,在于和 sealed 接口组合时,编译器会强制你穷举所有子类型——少写一个分支就编译不过:
sealed interface Shape permits Circle, Rect, Triangle {}
record Circle(double r) implements Shape {}
record Rect(double w, double h) implements Shape {}
record Triangle(double a, double b, double c) implements Shape {}
double area(Shape s) {
return switch (s) { // 编译器强制穷举
case Circle c -> Math.PI * c.r() * c.r();
case Rect r -> r.w() * r.h();
case Triangle t -> heron(t.a(), t.b(), t.c());
};
}
3.4 守卫模式的短路与可读性
when 守卫本质上是一个布尔短路条件:只有当类型匹配且守卫为真时分支才命中,因此它天然适合”先按类型分流、再按值域细分”的场景,把原本要嵌套在分支体内的 if 拉平到同一行。这既少一层缩进,也让分支的优先级一眼可见。注意守卫里不要写带副作用的逻辑,否则分支匹配顺序会变得难以推理。
四、Record 模式:解构数据(Java 21 正式)
Record 模式允许在 switch 中直接解构 Record 的组件,甚至嵌套解构,把”取出字段”这一步也交给编译器。它和我们在 Java 21 Record 深入实战 里讲的 Record 用法天然互补:Record 负责让数据不可变且自带访问器,Record 模式负责在消费时优雅地拆开它。
record Point(int x, int y) {}
record Line(Point start, Point end) {}
int manhattan(Line l) {
return switch (l) {
case Line(Point(int x1, int y1), Point(int x2, int y2)) ->
Math.abs(x1 - x2) + Math.abs(y1 - y2);
};
}
五、实战重构:一段老代码的前后对比
下面是一个消息处理器的常见写法——根据消息类型拼出描述。旧版本用一连串 if/else 加强转,重构后借助 sealed 接口与模式匹配,逻辑扁平且由编译器担保完整性:
// 旧写法
Object msg = receive();
String result;
if (msg instanceof LoginMessage) {
LoginMessage m = (LoginMessage) msg;
result = "login:" + m.getUser();
} else if (msg instanceof LogoutMessage) {
LogoutMessage m = (LogoutMessage) msg;
result = "logout:" + m.getSession();
} else if (msg instanceof PayMessage) {
PayMessage m = (PayMessage) msg;
result = "pay:" + m.getAmount();
} else {
result = "unknown";
}
// 新写法:sealed + 模式匹配
sealed interface Msg permits LoginMsg, LogoutMsg, PayMsg {}
record LoginMsg(String user) implements Msg {}
record LogoutMsg(String session) implements Msg {}
record PayMsg(long amount) implements Msg {}
String handle(Msg msg) {
return switch (msg) { // 穷举由编译器担保
case LoginMsg(String user) -> "login:" + user;
case LogoutMsg(String session) -> "logout:" + session;
case PayMsg(long amount) -> "pay:" + amount;
};
}
| 维度 | 旧写法 | 模式匹配 |
| 可读性 | 多层强转、缩进深 | 一行表达、扁平清晰 |
| 安全性 | 强转可能抛异常 | 编译器担保类型正确 |
| 代码行数 | 多 | 通常少 40%~60% |
| 穷举检查 | 无,漏分支靠人盯 | sealed+switch 强制穷举 |
六、常见坑与版本边界
模式匹配横跨多个 Java 版本,落地前务必确认运行环境的 JDK 版本,避免把预览语法带上生产:
| 特性 | 最低正式版本 | 注意点 |
| instanceof 模式匹配 | Java 16 | 模式变量作用域仅限为真分支 |
| switch 类型模式 | Java 21 | 预览期 17–20,生产直接用 21 |
| 守卫 when | Java 21 | 预览期 17–20 |
| Record 模式 | Java 21 | 可与 switch 嵌套解构 |
| null 处理 | — | 类型模式默认不匹配 null,需显式 case null |
两个高频坑:一是预览特性在 Java 17–20 需要 --enable-preview,且编译和运行的 JDK 版本必须一致,否则直接报错;二是类型模式不匹配 null,switch (obj) 收到 null 时不会进入任何类型分支,需要单独写 case null 才能安全兜底。
七、迁移建议
模式匹配不必一次性大改存量代码,推荐”新代码直接用、老代码在重构时顺手改”的渐进策略。它和现代 Java 的其他特性是同一套语言的红利:配合 Java 21 虚拟线程、结构化并发、Java 21 Record,可以组成一套干净、安全、易并发的现代 Java 编码范式。如果你的项目还停在 Java 8/11,可以先参考 Spring Boot 3 升级踩坑实录 把基线升到 21;高并发场景下的线程参数仍可沿用 线程池调优 的经验,日常编码提效也能结合 VS Code / JetBrains 提效技巧。
落到团队层面,可以分三步走:第一步,把 CI 编译基线锁到 JDK 21,并在构建文件里声明 release 21,从工具链层面杜绝预览语法混入生产;第二步,在 Code Review 清单里加一条”新代码优先用模式匹配替代 instanceof 强转”,让团队形成肌肉记忆;第三步,挑一个边界清晰的旧模块做试点重构,用单测保底后再逐步推广。IntelliJ IDEA 等主流 IDE 已经会把 instanceof 强转主动标记为可简化,改起来成本很低。
八、模式匹配落地自查清单
| 动作 | 是否完成 |
| 编译基线锁定 JDK 21(release 21) | ☐ |
| 新代码用 instanceof 模式替代强转 | ☐ |
| 类型分发改用 switch 类型模式 | ☐ |
| 密封类 + switch 开启编译器穷举检查 | ☐ |
| Record 消费处用 Record 模式解构 | ☐ |
总结一句:模式匹配不是语法糖那么简单,它把”类型判断 + 数据解构 + 穷举检查”三件事一次性交给了编译器。越早把 instanceof 强转、switch 常量堆砌换成模式匹配,你的 Java 代码就越短、越稳、越不容易在线上翻车。




