2335 字
约 7 分钟
13
S04-A21 · `switch` 表达式与模式匹配:`yield` 之外的那几个约束

一段代码用 switch 表达式替换了一长串 if-else,写得很清爽。而编译时它报"not exhaustive"——因为枚举多了两个值,而 switch 表达式要求穷尽所有分支(除非有 default)。这个要求在 if-else 里不存在,于是"迁移到 switch"时最容易漏。

switch 表达式(Java 14+)带来的不只是"更短的写法",还有三条新的编译期约束。理解它们才知道"什么时候该用表达式,什么时候该用语句"。


1. 表达式与语句的四点差别

维度 语句(旧) 表达式(新)
返回值 无 必须有
穷尽性 不要求 要求(或 default)
穿透 默认穿透 不穿透
分支写法 case X: case X ->

第三项是"安全"的改进:-> 形式不会穿透,于是"忘了 break"这类错误从语法上消失。第三项的"历史"是 switch 最著名的坑:忘了 break 会"继续往下执行",而这个错误在代码评审里极难看出——因为"缩进看起来是对的"。

第二项是"新约束":表达式必须"有值",于是"某个分支没覆盖"会编译失败——而 if-else 里"漏一个分支"只是"返回 null"。

2. 三种"穷尽性"的写法

写法 适用
覆盖所有枚举值 枚举(推荐,改动时编译器会提醒)
default 兜底 值域开放(字符串/数字)
case null 显式处理 允许 null 输入时

第一项的"前提"是"枚举是唯一的输入来源":如果入参是 String 或 int,那"穷尽"无从定义——于是只能用 default。
第一项的价值在于"将来加枚举值时,编译器会报错"——于是"忘记处理新值"变成编译期问题,而不是线上 NPE。

第二项的代价就在于此:default 会吸收新枚举值,于是"加了新值但走了默认分支",而编译器不报错。第二项的"折中"是"default -> throw new IllegalStateException(...)":它仍然编译通过,但"新值"会在运行期立刻炸——而不会"安静地走默认"。

// 反例:default 把"未来新增的枚举值"吞掉
String label(Status s) {
    return switch (s) {
        case CREATED -> "待支付";
        case PAID -> "已支付";
        default -> "未知";          // 新增 REFUNDED 后 → "未知"
    };
}
// 正解:显式覆盖,让编译器提醒
String label2(Status s) {
    return switch (s) {
        case CREATED -> "待支付";
        case PAID -> "已支付";
        case REFUNDED -> "已退款";   // 新增值时这里会编译报错
    };
}

3. yield 与代码块

写法 说明
case X -> expr 单表达式
case X -> { ... yield v; } 代码块 + yield
yield 在语句里 不合法

第二项的"另一种坑"是"代码块里有多条 return 与一个 yield":编译能过,但"到底哪个生效"要看路径——而它比"单表达式"难读。
第二项的"坑"是"忘了 yield":代码块里没有 yield 会编译失败(这反而是好事)。而"写了 yield 又在外面写 return"会造成"两套返回路径",容易看错。

// 代码块里必须 yield
int n = switch (type) {
    case "a" -> 1;
    case "b" -> {
        int extra = compute();
        yield 2 + extra;             // 必须 yield
    }
    default -> throw new IllegalArgumentException(type);
};

4. 模式匹配的三种用法(Java 17–21)

用法 例子
类型模式 case String s -> s.length()
守卫(when) case String s when s.isEmpty() -> ...
记录模式 case Point(int x, int y) -> ...

第二项(守卫)解决"类型匹配后还要判断"的问题:它把"类型 + 条件"写在同一行第二项的"顺序"要注意:守卫写在"类型模式之后",而它的条件可以引用绑定变量(case String s when s.isEmpty())——于是"类型与条件"共享同一个变量。
,于是"分支顺序"更清楚。

第三项的"代价"是"需要 record":普通类没有"解构"能力,于是它只适用于"值对象用 record 建模"的项目。
第三项(记录解构)在"值对象"上很实用:case Point(int x, int y) 直接拿到字段,不必再调 x()。

// 守卫 + 记录模式(Java 21)
String describe(Object o) {
    return switch (o) {
        case Point(int x, int y) when x == 0 && y == 0 -> "原点";
        case Point(int x, int y) -> "点(" + x + "," + y + ")";
        case null -> "空";
        default -> "其它";
    };
}

"分支数量多"的那条边界可以用"查表"落地,而它比 switch 更好维护:

// 用 Map 替代长 switch(分支多、且"值 → 行为")
static final Map<Status, Handler> HANDLERS = Map.of(
    CREATED, Handler::onCreated,
    PAID,    Handler::onPaid,
    REFUNDED,Handler::onRefunded
);
Handler h = HANDLERS.get(status);            // 未知值 → null
if (h == null) throw new IllegalStateException("未处理的状态: " + status);
// 收益:新增状态只改 Map(一处),且"漏注册"在启动或首个请求就暴露

5. 什么时候用表达式、什么时候用语句

情形 选择
每个分支"产生一个值" 表达式
分支里有"副作用为主"的逻辑 语句(更清楚)
分支数量多(> 8) 考虑"查表"或"多态"
需要 fall-through 语句(表达式不支持)

第三项是"边界":switch 超过八个分支,"值映射"往往比"分支"更清楚(如用 Map<Status, Handler>)。

第四项提醒"两者不能混":表达式不支持穿透,所以"多个 case 共享一段逻辑"要写成 case A, B -> ...。第四项的"写法"很干净:case A, B -> ... 比"两个 case 穿透"更明确——而它顺便消除了"漏 break"的可能。

"迁移"时的等价性检查可以很机械:列出原 if-else 的所有条件,逐条对照新 switch 的 case。

// 一次迁移的检查表(四行)
1 原 if 链的每个条件 → 新 case 是否存在(含"多个条件合并"的情况)
2 原"漏写"的兜底分支(else / 最后 return)→ 新 default 是否存在
3 原"穿透"语义(多个 case 共享逻辑)→ 新 `case A, B ->` 是否等价
4 原"副作用顺序" → 新分支内的顺序是否一致
// 缺第 3 项时,最常出现"行为悄悄变了"

6. 排错:五种"switch 迁移问题"

现象 成因 处理方向
编译报"not exhaustive" 漏枚举值/无 default 补分支
新增枚举值走了"未知" 用了 default 去掉 default
yield 报错 代码块缺 yield 补 yield
行为变了(原来穿透) -> 不穿透 合并 case 或用语句
分支顺序导致误匹配 类型模式从上到下 具体的放前面

第五项的"编译器行为"值得知道:它会报"pattern dominated"类警告(部分版本),而不总是错误——所以要开全警告并当成错误。
第五项在"类型模式"里很关键:case Object o 写在最前面会吞掉所有分支——而它编译通过,只是"永远走第一个"。

最后一个"版本"边界值得写清:switch 表达式在 Java 14 转正,模式匹配在 21 转正。跨版本项目要按"最低支持的 JDK"决定可用语法——而不是"我本地能编过"。

// 三个版本分界(决定能否用)
Java 14:switch 表达式(-> / yield)
Java 17:类型模式(instanceof 模式 + switch 预览)
Java 21:switch 模式匹配 + 记录模式(正式)
// 判据:项目 pom 里的 <release> 与运行环境实际版本

7. 边界:什么情况不必用新语法

场景 为什么 做法
老项目(Java 8/11) 语法不支持 用 if-else
分支很少(2–3 个) 表达式收益小 随意
需要穿透的旧逻辑 语义不同 保留语句
团队不熟 可读性风险 先小范围用

判断口径一句话:"每个分支是不是在'算一个值'"。最后一条与"可读性"有关的边界:"表达式"的优势在"不可穿透 + 可嵌入"——它能直接放进赋值/参数位置,而语句不行。所以"要一个值"时,表达式省掉一个临时变量。
是(映射、转换、分类),表达式更清楚且更安全;不是(执行一串动作),语句更好。


8. 自测

  1. 表达式与语句的四点差别里,哪一条是"新约束"?哪一条是"安全改进"?
  2. 为什么"用 default 兜底"会在"新增枚举值"时出问题?
  3. yield 用在什么地方?"代码块里忘了 yield"会怎样?
  4. 模式匹配的三种用法里,哪一种解决了"类型匹配后还要判断"?举一个例子。
  5. 为什么"case Object o 写在最前面"会让所有分支失效?
  6. 判断"用表达式还是语句"的那个问题是什么?
S04-A21 · `switch` 表达式与模式匹配:`yield` 之外的那几个约束
https://www.clxhxhhr.top/posts/6598/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。