JDK JDK 学习手册8 → 27 · JDK 专用学习站
DEEPDIVE · LANGUAGE · JEP 440 / 441

模式匹配:让数据形状成为控制流

JDK 21 · 深钻主题 02 / 08 · 预计 50 分钟
核心四步深钻JEP 440JEP 441JEP 443
一句话模式匹配把「判断类型 → 强转 → 取字段」三步合并为一次声明:if (o instanceof Point(int x, int y))。在 switch 中配合 sealed 接口,还能让编译器检查分支是否穷尽。
本质:以前代码围绕「对象是什么类」展开;现在代码围绕「数据是什么形状」展开。类型检查和字段提取合并,控制流因此可以直接描述数据结构。

学完你要能自己回答

  1. instanceof 模式变量的作用域为什么能延伸到 && 右侧,却不能越过 ||?
  2. switch 的模式匹配在什么情况下算穷尽?编译器如何检查?
  3. record pattern 的解构是浅拷贝还是引用?嵌套 record 怎么写?
  4. case null 不写会怎样?case null, default 又是什么意思?
  5. 为什么说模式匹配让 sealed 接口真正「闭环」?
01 从「问类型」到「描述形状」02 三个心智锚点

从「问类型」到「描述形状」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 里抛异常可靠得多。

01 switch 模式匹配的完整规则02 record pattern 到底解构了什么03 null、穷尽性与向后兼容

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;普通类的字段提取仍需访问器或类型模式。
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 会发生什么?
重新编译时编译失败,提示 switch 未覆盖新子类型。这正是穷尽性检查的价值:把「忘记处理新情况」从运行时异常提前到编译期。若是运行期替换了接口 jar 而未重新编译调用方,则可能抛 MatchException。
01 重构:用模式匹配替换 visitor02 可运行示例:形状分类器

重构:用模式匹配替换 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 先猜再看02 费曼自检:讲清规则

先猜再看01

先猜再看case CharSequence cs 放在 case String s 前面,编译能过吗?
能过,但 String 分支永远不可达,编译器会报 dominance 错误:更宽泛的模式已经覆盖了 String。应把更具体的 String 放在前面。
先猜再看record pattern 解构出来的组件是副本吗?
不是深拷贝。基本类型按值绑定,引用类型绑定的是同一个引用。对不可变 record 通常无差别;如果组件是可变对象,修改它会影响原对象。
先猜再看编译器能对 switch (Object o) 做穷尽性检查吗?
不能,Object 不是 sealed/enum,编译器无法枚举所有子类型,必须写 default。只有封闭类型(sealed 接口、enum)才可能省略 default。

费曼自检:讲清规则02

01. 为什么模式变量能用在 && 右侧,却不能用在 || 右侧?
因为 && 右侧只在左侧为 true(匹配成功)时求值;|| 右侧在左侧失败时也会求值,此时模式变量未绑定。
02. sealed 接口和 switch 穷尽性是什么关系?
sealed 给出编译期已知的封闭子类型集合,switch 覆盖全部子类型才算穷尽;新增子类型会让相关 switch 编译失败,从而强制更新。
03. case null, default 与单独 case null + default 有何不同?
语义等价,前者把 null 与默认分支合并,减少重复;注意 default 本身不匹配 null,必须显式写 null。
04. 模式 switch 为什么不允许 fall through?
模式分支通常带绑定变量,贯穿会让变量作用域和初始化状态变得复杂;箭头语法强制每个分支独立,避免隐式落入下一分支。
05. 什么时候仍应使用 visitor 而不是模式 switch?
当数据类型开放(无法 sealed)或操作集合频繁扩展、类型稳定时,visitor 的双分派更合适;模式 switch 适合封闭类型 + 操作较少的场景。
学习进度 记录保存在本机浏览器 · 不上传

JDK 学习手册 · JDK / Java 专用学习站 · 纯静态 · 双击 index.html 即可使用

本站专注 JDK 版本演进与 Java 教学,非 Oracle / OpenJDK 官方站点。版本数据来自 OpenJDK 官方 Features 表;课程示例已在对应本机 JDK 实测。构建日期 2026-11-12。