Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
大翔(CTO):咱们的支付系统里有一堆
if-else判断消息类型,看着就头疼。白歌(架构师):JDK 21 的 Switch 模式匹配可以彻底解决这个问题,配合 sealed class,编译器还能帮我们检查穷尽性。
小崔(后端开发):之前 switch 只能匹配常量,现在能直接匹配类型了?
孔蓝(后端新人):那 null 值怎么办?以前 switch 碰到 null 直接 NPE……
传统 switch 的局限性
| 局限性 | 说明 |
|---|---|
| 只能匹配常量 | case 后只能是常量表达式(字面量、enum 常量、String 常量) |
| 需要 break | 忘记写 break 会 fall through,引发逻辑错误 |
| 无法直接处理类型 | 必须先 instanceof 再强转,代码冗长 |
| 不能处理 null | switch (obj) 中 obj 为 null 直接抛 NPE |
| 无穷尽性检查 | 编译器无法确保你覆盖了所有情况 |
传统写法的问题示意:
// === 场景说明:传统写法,需要先 instanceof 再强转 ===
class TraditionalSwitch {
public static void main(String[] args) {
Object obj = "Hello";
String result;
if (obj instanceof String) {
String s = (String) obj;
result = "字符串,长度:" + s.length();
} else if (obj instanceof Integer) {
Integer i = (Integer) obj;
result = "整数,值:" + i;
} else if (obj == null) {
result = "null 值";
} else {
result = "未知类型";
}
System.out.println(result);
}
}
输出:
字符串,长度:5
问题:
if-else链冗长,instanceof后还要强转,代码重复且不优雅。
Switch 模式匹配基础语法
| 概念 | 定义 |
|---|---|
| 模式匹配 Switch | 在 case 标签中直接声明变量,编译器自动完成类型匹配和绑定 |
| 守卫条件(Guard) | 用 when 关键字在 case 后附加布尔条件,进一步细化匹配 |
| 穷尽性检查 | 编译器确保所有可能的类型/值都被覆盖,否则编译报错 |
基本语法
// === 场景说明:使用模式匹配 switch 处理不同类型 ===
class SwitchPatternMatching {
public static void main(String[] args) {
Object obj = 42;
String result = switch (obj) {
case String s -> "字符串:" + s + ",长度:" + s.length();
case Integer i -> "整数:" + i + ",平方:" + (i * i);
case Double d -> "浮点数:" + d;
case null, default -> "null 或其他类型";
};
System.out.println(result);
}
}
输出:
整数:42,平方:1764
要点:模式变量(
String s、Integer i)在->右侧的作用域中自动可用,无需手动强转。
守卫条件(Guard Clauses)
JDK 20 引入 when 关键字,允许在 case 后附加条件。
| 语法 | 说明 |
|---|---|
case Integer i when i > 0 -> ... | 仅当 i > 0 时匹配 |
case String s when s.length() > 5 -> ... | 可结合模式变量的属性 |
// === 场景说明:使用 when 守卫条件细化匹配 ===
class GuardClauseDemo {
public static void main(String[] args) {
Object obj = -10;
String result = switch (obj) {
case Integer i when i > 0 -> "正整数:" + i;
case Integer i when i == 0 -> "零";
case Integer i when i < 0 -> "负整数:" + i;
case String s when s.isEmpty() -> "空字符串";
case String s -> "非空字符串:" + s;
case null -> "null";
default -> "其他类型";
};
System.out.println(result);
}
}
输出:
负整数:-10
null 处理
传统 switch 中,switch (obj) 若 obj 为 null 直接抛 NullPointerException。
模式匹配 switch 允许显式处理 null。
| 写法 | 说明 |
|---|---|
case null -> ... | 显式匹配 null 值 |
case null, default -> ... | null 和非匹配类型合并处理 |
// === 场景说明:显式处理 null 值,避免 NPE ===
class NullHandlingDemo {
public static void main(String[] args) {
Object obj = null;
String result = switch (obj) {
case null -> "⚠ 接收到 null 值,已安全处理";
case String s -> "字符串:" + s;
case Integer i -> "整数:" + i;
default -> "其他类型:" + obj.getClass().getSimpleName();
};
System.out.println(result);
}
}
输出:
⚠ 接收到 null 值,已安全处理
注意:如果
switch块中没有处理null的case,而传入null,仍然会抛NullPointerException。case null是可选的,但建议显式处理。
多个模式合并
同一处理逻辑可以合并多个 case 标签,用逗号分隔。
// === 场景说明:多个类型共用同一处理逻辑 ===
class MultiPatternDemo {
public static void main(String[] args) {
Object obj = 3.14;
String result = switch (obj) {
case String s -> "文本类型:" + s;
case Integer i, Double d -> "数值类型:" + obj;
case Boolean b -> "布尔类型:" + b;
case null -> "null";
default -> "未处理的类型";
};
System.out.println(result);
}
}
输出:
数值类型:3.14
穷尽性检查 + Sealed Class
| 概念 | 定义 |
|---|---|
| 穷尽性检查(Exhaustiveness) | 编译器验证 switch 覆盖了所有可能的情况,未覆盖则编译报错 |
| Sealed Class | 限制哪些类可以继承/实现该类,与 switch 模式匹配配合可实现编译期穷尽性验证 |
Sealed Class 语法回顾
// === 场景说明:定义密封的事件体系 ===
// Event.java
sealed interface Event permits OrderEvent, PaymentEvent, RefundEvent { }
final class OrderEvent implements Event {
private final String orderId;
public OrderEvent(String orderId) { this.orderId = orderId; }
public String getOrderId() { return orderId; }
}
final class PaymentEvent implements Event {
private final double amount;
public PaymentEvent(double amount) { this.amount = amount; }
public double getAmount() { return amount; }
}
final class RefundEvent implements Event {
private final String reason;
public RefundEvent(String reason) { this.reason = reason; }
public String getReason() { return reason; }
}
配合 Switch 模式匹配的穷尽性
// === 场景说明:sealed class + switch,编译器确保穷尽性 ===
class EventProcessor {
public static void main(String[] args) {
Event event = new PaymentEvent(199.99);
String result = switch (event) {
case OrderEvent e -> "处理订单:" + e.getOrderId();
case PaymentEvent e -> "处理支付,金额:" + e.getAmount();
case RefundEvent e -> "处理退款,原因:" + e.getReason();
// 无需 default!编译器知道 Event 只有 3 个子类,已全部覆盖
};
System.out.println(result);
}
}
输出:
处理支付,金额:199.99
如果新增子类但未更新 switch:编译器会报错,提示
switch表达式未覆盖所有情况,在编译期发现错误,而非运行期。
实战场景:飞翔科技 Kafka 消息处理器
大翔(CTO):我们的 Kafka 消费者需要处理订单、支付、退款三种消息,用 sealed class + switch 模式匹配来路由。
完整实现
// === 场景说明:飞翔科技 Kafka 消息处理器,使用 sealed class + switch 模式匹配 ===
// 消息基类(密封接口)
sealed interface KafkaMessage permits OrderMsg, PaymentMsg, RefundMsg {
String getTopic();
}
// 订单消息
record OrderMsg(String orderId, double amount, String topic) implements KafkaMessage {
public OrderMsg {
if (amount < 0) throw new IllegalArgumentException("金额不能为负");
}
}
// 支付消息
record PaymentMsg(String paymentId, double amount, String topic) implements KafkaMessage { }
// 退款消息
record RefundMsg(String orderId, String reason, String topic) implements KafkaMessage { }
// 消息处理器
class KafkaMessageHandler {
public static String handle(KafkaMessage message) {
return switch (message) {
case OrderMsg m when m.amount() > 10000 ->
"🚨 大额订单告警,订单号:" + m.orderId() + ",金额:" + m.amount();
case OrderMsg m ->
"📦 处理订单,订单号:" + m.orderId() + ",金额:" + m.amount();
case PaymentMsg m when m.amount() <= 0 ->
"❌ 支付金额异常:" + m.paymentId();
case PaymentMsg m ->
"💰 处理支付,支付号:" + m.paymentId() + ",金额:" + m.amount();
case RefundMsg m ->
"↩️ 处理退款,订单号:" + m.orderId() + ",原因:" + m.reason();
};
}
public static void main(String[] args) {
KafkaMessage msg1 = new OrderMsg("ORD-20240601-001", 299.00, "order.topic");
KafkaMessage msg2 = new OrderMsg("ORD-20240601-002", 25000.00, "order.topic");
KafkaMessage msg3 = new PaymentMsg("PAY-001", 199.99, "payment.topic");
KafkaMessage msg4 = new RefundMsg("ORD-20240601-001", "商品质量问题", "refund.topic");
System.out.println(handle(msg1));
System.out.println(handle(msg2));
System.out.println(handle(msg3));
System.out.println(handle(msg4));
}
}
输出:
📦 处理订单,订单号:ORD-20240601-001,金额:299.0
🚨 大额订单告警,订单号:ORD-20240601-002,金额:25000.0
💰 处理支付,支付号:PAY-001,金额:199.99
↩️ 处理退款,订单号:ORD-20240601-001,原因:商品质量问题
与 JDK 14 Switch 表达式的对比
| 特性 | JDK 14 Switch 表达式 | JDK 21 Switch 模式匹配 |
|---|---|---|
箭头语法 -> | ✅ 支持 | ✅ 支持 |
| 返回值 | ✅ 可作为表达式返回值 | ✅ 支持 |
| 类型匹配 | ❌ 不支持 | ✅ case String s -> |
守卫条件 when | ❌ 不支持 | ✅ 支持 |
| null 处理 | ❌ 不支持 | ✅ case null -> |
| 穷尽性(sealed) | ❌ 不支持 | ✅ 编译器检查 |
| 多模式合并 | ❌ 不支持 | ✅ case A, B -> |
易错场景
错误 1:忘记处理 null
// ❌ 错误:未处理 null,传入 null 会抛 NPE
class NullMissed {
public static void main(String[] args) {
Object obj = null;
String result = switch (obj) {
case String s -> s;
case Integer i -> String.valueOf(i);
default -> "other";
};
System.out.println(result);
}
}
// 运行结果:Exception in thread "main" java.lang.NullPointerException
// ✅ 正确:显式处理 null
class NullHandled {
public static void main(String[] args) {
Object obj = null;
String result = switch (obj) {
case null -> "null 已处理";
case String s -> s;
case Integer i -> String.valueOf(i);
default -> "other";
};
System.out.println(result);
}
}
// 运行结果:null 已处理
错误 2:守卫条件中的变量作用域
// ❌ 错误:when 中引用了未绑定的变量
// case Integer i when s.length() > 0 -> ... // s 在此分支不存在
// ✅ 正确:when 中只使用当前 case 绑定的变量
class GuardScope {
public static void main(String[] args) {
Object obj = "Hello";
String result = switch (obj) {
case String s when s.length() > 3 -> "长字符串:" + s;
case String s -> "短字符串:" + s;
default -> "其他";
};
System.out.println(result);
}
}
// 运行结果:长字符串:Hello
错误 3:穷尽性不足(非 sealed 类型)
// ❌ 错误:Object 类型有无数种可能,编译器无法确保穷尽,必须加 default
class NotExhaustive {
public static void main(String[] args) {
Object obj = 1;
// 编译错误:the switch expression does not cover all possible input values
// String result = switch (obj) {
// case String s -> s;
// case Integer i -> String.valueOf(i);
// };
}
}
// ✅ 正确:加 default 分支确保穷尽
class ExhaustiveFixed {
public static void main(String[] args) {
Object obj = 1;
String result = switch (obj) {
case String s -> s;
case Integer i -> String.valueOf(i);
default -> "未匹配的类型:" + obj.getClass().getSimpleName();
};
System.out.println(result);
}
}
// 运行结果:1
面试考点
问题 1:Switch 模式匹配与传统 switch 相比有哪些核心改进?
答:核心改进有五点:
- 类型模式匹配:
case String s直接在 case 中完成类型匹配和变量绑定,无需先instanceof再强转。- 守卫条件:
when关键字允许在 case 后附加布尔表达式,进一步细化匹配条件。- null 安全:
case null ->可以显式处理 null 值,避免隐式 NPE。- 多模式合并:
case Integer i, Double d ->允许多个类型共用同一处理逻辑。- 穷尽性检查:配合
sealed class,编译器在编译期确保所有可能类型都被覆盖,无需手写default。
问题 2:sealed class 结合 switch 模式匹配的优势是什么?
答:优势体现在三个方面:
- 编译期安全:新增子类时,若未同步更新
switch分支,编译器直接报错,将错误消灭在编译期。- 无需
default:当switch覆盖了sealed接口的所有permits子类时,编译器认为已穷尽,可以不写default,避免default吞掉新增类型的处理逻辑。- 代码即文档:
sealed声明明确了类型的扩展边界,switch分支与permits列表一一对应,可读性极强。
问题 3:when 守卫条件与在 case 右侧写 if 有什么区别?
答:区别在于匹配阶段和穷尽性影响:
- 匹配阶段:
when是在模式匹配成功后、进入分支前评估的,不匹配的会继续尝试下一个case;而if是在分支内部判断,不匹配则需要通过break或return跳出。- 穷尽性:
when守卫的分支仍被视为对该类型的一种匹配尝试,编译器在检查穷尽性时会考虑所有case;if-else在分支内部不影响switch的匹配结构。- fall-through 语义:
when不匹配时自动尝试下一个case,更符合switch的匹配语义;if需要手动控制流程。
总结
白歌(架构师):用 sealed class 定义封闭类型体系,用 switch 模式匹配做路由,编译期就能发现遗漏的分支,这才是类型安全的代码。
孔蓝(后端新人):那我以后写消息处理器,再也不用担心漏掉某个类型了!
大翔(CTO):记住一句话——让编译器帮你干活,而不是让 Bug 在运行期帮你干活。
"Programs must be written for people to read, and only incidentally for machines to execute." —— Harold Abelson, 《Structure and Interpretation of Computer Programs》