模式匹配:让数据形状成为控制流
if (o instanceof Point(int x, int y))。在 switch 中配合 sealed 接口,还能让编译器检查分支是否穷尽。学完你要能自己回答
instanceof模式变量的作用域为什么能延伸到&&右侧,却不能越过||?- switch 的模式匹配在什么情况下算穷尽?编译器如何检查?
- record pattern 的解构是浅拷贝还是引用?嵌套 record 怎么写?
case null不写会怎样?case null, default又是什么意思?- 为什么说模式匹配让 sealed 接口真正「闭环」?
从「问类型」到「描述形状」01
传统写法里,类型判断、强制转换和字段访问是三件分开的事:
if (obj instanceof Point) {
Point p = (Point) obj;
int x = p.x();
int y = p.y();
System.out.println(x + "," + y);
}
模式匹配把这三步压成一条声明。Point(int x, int y) 不是构造调用,而是一个模式:先测试 obj 是否是 Point,是则解构出两个组件并绑定到 x、y。
if (obj instanceof Point(int x, int y)) {
System.out.println(x + "," + y); // x、y 已绑定,无需强转
}
| 概念 | 回答的问题 | 例子 |
|---|---|---|
| 类型模式 | 是不是这个类型? | case String s |
| record 模式 | 是不是这个形状?组件是什么? | case Point(int x, int y) |
| 守卫 | 匹配后还满足什么条件? | case Circle(double r) when r > 0 |
| 穷尽性 | 所有可能都覆盖了吗? | sealed / enum 的 switch 不写 default |
三个心智锚点02
锚点一:模式变量有「流作用域」
模式变量只在确定匹配成功的路径上可见。if (o instanceof String s && s.length() > 0) 中,&& 右侧一定在匹配成功后执行,所以 s 可见;|| 右侧则可能因左侧失败而到达,s 不可见。
if (o instanceof String s && !s.isEmpty()) {
System.out.println(s.toUpperCase()); // 合法
}
if (!(o instanceof String s)) return;
System.out.println(s.length()); // 合法:提前返回后,s 必然已绑定
锚点二:switch 的分支顺序就是匹配优先级
switch 从上到下匹配,第一个命中的分支获胜。因此更具体的模式必须写在更宽泛的模式前面,否则编译器会报「dominance」错误。case Object o 会吞掉后面所有分支。
锚点三:穷尽性让编译器替你查漏
当 selector 是 sealed 接口或 enum,且所有 permitted 子类型/枚举常量都被覆盖时,switch 可以不写 default。新增一个子类型后,所有相关 switch 会编译失败——这比运行时 default 里抛异常可靠得多。
switch 模式匹配的完整规则01
sealed interface Shape permits Circle, Rect, Square {}
record Circle(double r) implements Shape {}
record Rect(double w, double h) implements Shape {}
record Square(double side) implements Shape {}
static double area(Shape s) {
return switch (s) {
case Circle(double r) -> Math.PI * r * r;
case Rect(double w, double h) -> w * h;
case Square(double side) -> side * side;
// 无需 default:sealed 已穷尽
};
}
- selector 类型:引用类型、enum、String,以及原有的 int/short/byte/char。
- case 标签:常量、类型模式、record 模式、
null,可以组合成case null, default。 - 守卫:
case Circle(double r) when r > 10 -> ...;守卫在模式匹配成功后求值。 - 箭头分支:模式 switch 只能使用
->,不会贯穿(fall through)。 - 穷尽性:sealed 类型要求覆盖所有 permitted 子类型;enum 要求覆盖所有常量(或提供 default)。
如果运行时的实际类型不在编译期可见的 permitted 集合里(例如二进制不兼容的类路径),穷尽 switch 会抛出 MatchException,而不是悄悄走进 default。
record pattern 到底解构了什么02
record pattern 调用组件的访问器,把值绑定到模式变量。对引用类型组件,绑定的是引用,不是深拷贝。
record Point(int x, int y) {}
record Line(Point start, Point end) {}
static String describe(Object o) {
return switch (o) {
case Line(Point(var x1, var y1), Point(var x2, var y2))
when x1 == x2 && y1 == y2 -> "退化线段";
case Line(Point(var x1, var y1), Point(var x2, var y2)) ->
"从 (" + x1 + "," + y1 + ") 到 (" + x2 + "," + y2 + ")";
case Point(var x, var y) -> "点 (" + x + "," + y + ")";
default -> "其他";
};
}
var让编译器推断组件类型;也可以写Point(int x, int y)显式声明。- 嵌套模式会递归匹配:外层 record 匹配成功后,再匹配每个组件。
- 泛型 record 的类型参数可由模式推断,例如
Box<String>(String s)。 - record pattern 只能用于 record;普通类的字段提取仍需访问器或类型模式。
null、穷尽性与向后兼容03
static String classify(String s) {
return switch (s) {
case null -> "空值";
case "" -> "空串";
case String str when str.startsWith("J") -> "J 开头";
default -> "普通字符串";
};
}
- 不写
case null时,selector 为 null 会直接抛NullPointerException(与旧 switch 行为一致)。 case null必须放在default之前;也可以合并成case null, default -> ...。- 类型模式
case Object o不匹配 null,null 需要单独处理。 - 守卫里可以使用模式变量,但模式变量在守卫失败后不会泄漏到后续分支。
先猜再看sealed 接口新增一个 permitted 子类后,原来「穷尽」的 switch 会发生什么?
重构:用模式匹配替换 visitor01
对封闭的 sealed 数据结构,模式 switch 往往比 visitor 更短:新增操作只需加一个函数,不必修改每个元素类。代价是新增子类型时要改所有 switch——这正是 sealed 想让你面对的成本。
sealed interface Expr permits Num, Add, Mul {}
record Num(double value) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
record Mul(Expr left, Expr right) implements Expr {}
static double eval(Expr e) {
return switch (e) {
case Num(double v) -> v;
case Add(var l, var r) -> eval(l) + eval(r);
case Mul(var l, var r) -> eval(l) * eval(r);
};
}
- 用
var让嵌套模式更短;需要类型时再写显式模式。 - 递归求值不需要额外上下文对象,代码结构直接映射语法树。
- 如果操作会持续增加(打印、优化、类型检查),visitor 仍然有优势;如果类型封闭、操作较少,模式 switch 更直接。
可运行示例:形状分类器02
public class PatternDemo {
sealed interface Shape permits Circle, Rect, Square {}
record Circle(double r) implements Shape {}
record Rect(double w, double h) implements Shape {}
record Square(double side) implements Shape {}
static String classify(Shape s) {
return switch (s) {
case Circle(double r) when r <= 0 -> "非法圆";
case Circle(double r) when r > 100 -> "大圆 r=" + r;
case Circle(double r) -> "圆 r=" + r;
case Rect(double w, double h) when w == h -> "正方形(按矩形建模)";
case Rect(double w, double h) -> "矩形 " + w + "x" + h;
case Square(double side) -> "正方形 side=" + side;
};
}
public static void main(String[] args) {
System.out.println(classify(new Circle(5)));
System.out.println(classify(new Rect(2, 3)));
System.out.println(classify(new Square(4)));
System.out.println(classify((Shape) null));
}
}
圆 r=5.0 矩形 2.0x3.0 正方形 side=4.0 Exception in thread "main" java.lang.NullPointerException
最后一个调用故意传入 null:switch 没有 case null,因此抛 NPE。把 classify 的参数改成可能为 null 时,应显式加 case null -> "空值"。
先猜再看01
先猜再看case CharSequence cs 放在 case String s 前面,编译能过吗?
String 分支永远不可达,编译器会报 dominance 错误:更宽泛的模式已经覆盖了 String。应把更具体的 String 放在前面。先猜再看record pattern 解构出来的组件是副本吗?
先猜再看编译器能对 switch (Object o) 做穷尽性检查吗?
费曼自检:讲清规则02
01. 为什么模式变量能用在 && 右侧,却不能用在 || 右侧?
&& 右侧只在左侧为 true(匹配成功)时求值;|| 右侧在左侧失败时也会求值,此时模式变量未绑定。02. sealed 接口和 switch 穷尽性是什么关系?
03. case null, default 与单独 case null + default 有何不同?
default 本身不匹配 null,必须显式写 null。