Pattern Matching 模式匹配

这一章是现代 Java 三件套(Records + Sealed + Pattern Matching)的”最后一公里”——Pattern Matching(模式匹配)。它是 Java 21(2023 年 9 月)正式发布的特性(JEP 441),把 instanceofswitch 都”武装到了牙齿”,让 Java 终于能像 Scala、Rust 一样优雅地处理类型分支和数据解构。

一、为什么需要模式匹配:if-else 链的丑陋

1.1 传统 instanceof 的样板代码

Object obj = "hello";
if (obj instanceof String) {
    String s = (String) obj;   // 强转, 多余
    System.out.println(s.length());
}

明明 instanceof String 已经告诉我们 obj 是 String 了,还要写一次 String s = (String) obj——这是纯粹的样板。三行代码干一件事:判断 + 强转 + 使用。

1.2 复杂 if-else 链的灾难

处理多种类型时更惨:

String describe(Object obj) {
    if (obj instanceof String) {
        String s = (String) obj;
        return "字符串: " + s;
    } else if (obj instanceof Integer) {
        Integer i = (Integer) obj;
        return "整数: " + i;
    } else if (obj instanceof List) {
        List<?> list = (List<?>) obj;
        return "列表: " + list.size() + " 项";
    } else if (obj == null) {
        return "空";
    } else {
        return "未知: " + obj.getClass();
    }
}

四个 instanceof + 四次强转 + 四个 else if——又长又重复,眼花缭乱。这就是 Java 老牌框架(Struts、Spring MVC 的 handler)写起来啰嗦的根源之一。

1.3 模式匹配要解决什么

Pattern Matching 让”判断 + 强转 + 绑定变量”一步到位:

String describe(Object obj) {
    return switch (obj) {
        case null -> "空";
        case String s -> "字符串: " + s;
        case Integer i -> "整数: " + i;
        case List<?> l -> "列表: " + l.size() + " 项";
        default -> "未知: " + obj.getClass();
    };
}

判断和强转合一、变量自动绑定、switch 表达式直接返回——这就是模式匹配的威力。

二、instanceof 模式匹配(Java 16+)

最基础的模式匹配——instanceof 后面跟”类型 + 变量名”,匹配成功自动绑定:

2.1 基本用法

Object obj = "hello";
if (obj instanceof String s) {
    System.out.println(s.length());   // s 已绑定, 不用强转
}
// s 在这里不可见 (作用域只在 if 内)

s模式变量(pattern variable)——只在”匹配成功的分支”里有效。这叫”流敏感作用域(flow-sensitive scope)“。

2.2 流敏感作用域

模式变量的作用域不是简单的”花括号内”,而是”逻辑上一定能匹配到的路径”:

Object obj = "hello";
if (!(obj instanceof String s)) {
    return;   // 这里 s 还没绑定
}
// 这里 s 已绑定 (因为 obj 不是 String 早就 return 了)
System.out.println(s.length());   // 合法!
Object obj = Math.random() > 0.5 ? "hello" : Integer.valueOf(42);
if (obj instanceof String s && s.length() > 3) {
    // s 在 && 右侧已绑定, 可以用
    System.out.println(s);
}

编译器做”反向数据流分析”——只要某条路径上 instanceof 必然成功,变量就绑定。这让模式变量比普通局部变量更智能。

2.3 重构 equals

instanceof 模式匹配最常见的应用就是简化 equals

// 老写法
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Point)) return false;
    Point p = (Point) o;
    return p.x == this.x && p.y == this.y;
}

// 新写法
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Point p)) return false;   // 强转 + 绑定一步到位
    return p.x == this.x && p.y == this.y;
}

少一行,少一次”看走眼”的机会。

三、switch 模式匹配(Java 21)

switch 模式匹配是 Pattern Matching 的”重头戏”——case 不再只能匹配常量,可以匹配类型Record 结构null 等。

3.1 类型模式

String format(Object obj) {
    return switch (obj) {
        case null -> "null";
        case String s -> "字符串 \"" + s + "\"";
        case Integer i when i > 0 -> "正整数 " + i;
        case Integer i -> "非正整数 " + i;
        case int[] arr -> "int 数组, 长度 " + arr.length;
        case List<?> l when l.isEmpty() -> "空列表";
        case List<?> l -> "列表, 长度 " + l.size();
        default -> "其他类型: " + obj.getClass().getSimpleName();
    };
}

要点:

  • case String s —— 类型模式,匹配 String 且绑定到 s
  • case null —— 终于能匹配 null 了!传统 switch 遇到 null 直接 NPE,现在可以优雅处理。
  • when —— 守卫子句(guard),附加条件。
  • 多个 case 顺序敏感——上面的优先匹配,Integer i when i > 0 必须在 Integer i 前面。

3.2 守卫子句 when

when 给 case 加”附加条件”——类型匹配后,再判断条件:

return switch (obj) {
    case Integer i when i > 100 -> "大整数";
    case Integer i when i < 0 -> "负整数";
    case Integer i -> "普通整数";   // 兜底
    ...
};

when 的判断在类型匹配之后——所以 iwhen 里已绑定可用。这比传统 switch 的”嵌套 if”优雅得多。

3.3 case null:传统 switch 的痛点

传统 switch 遇到 null 直接抛 NPE——因为 obj.hashCode()switch (obj) 时就崩了。模式匹配的 switch 解决了这点:

String s = switch (obj) {
    case null -> "null";        // 显式处理 null
    case String str -> str;
    default -> "其他";
};

如果不写 case null,遇到 null 还是会 NPE(保持向后兼容),但你可以显式选择处理它。

四、Record 模式:解构 Record

Record 模式(JEP 440,Java 21)让你直接在 case 里”解构”Record 的组件:

4.1 基本 Record 模式

record Point(int x, int y) {}

String describe(Object obj) {
    return switch (obj) {
        case Point(int x, int y) -> "点 (" + x + ", " + y + ")";
        default -> "非点";
    };
}

case Point(int x, int y) 直接把 Point 的两个组件解构到 xy——不用 ((Point) obj).x()

4.2 嵌套 Record 模式

record Point(int x, int y) {}
record Rectangle(Point topLeft, Point bottomRight) {}

int area(Object obj) {
    return switch (obj) {
        case Rectangle(Point(int x1, int y1), Point(int x2, int y2)) ->
            Math.abs((x2 - x1) * (y2 - y1));
        default -> 0;
    };
}

两层嵌套一次解构——这是函数式语言的”模式匹配”在 Java 里的实现。在 AST 处理、JSON 解析树遍历里特别好用。

4.3 Record 模式 + when

return switch (shape) {
    case Circle(double r) when r > 0 -> Math.PI * r * r;
    case Circle(double r) -> 0;   // 半径非正, 面积 0
    case Rectangle(double w, double h) when w > 0 && h > 0 -> w * h;
    case Rectangle(double w, double h) -> 0;
    ...
};

4.4 Record 模式 + 类型推断

如果 Record 组件类型已知(如 record Box<T>(T value)),模式变量类型可推断:

record Box<T>(T value) {}

String open(Box<String> box) {
    return switch (box) {
        case Box(String s) -> "字符串: " + s;   // T 推断为 String
    };
}

五、穷举检查:sealed 的礼物

switch 模式匹配配合 Sealed Classes(上一章讲过),可以省略 default——编译器检查”是否穷举所有子类”:

sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double r) implements Shape {}
record Rectangle(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(double r) -> Math.PI * r * r;
        case Rectangle(double w, double h) -> w * h;
        case Triangle(double a, double b, double c) -> {
            double p = (a + b + c) / 2;
            yield Math.sqrt(p * (p - a) * (p - b) * (p - c));
        }
        // 不需要 default! 编译器知道只有这三种
    };
}

未来加 case PentagonShape.permits,所有 switch(Shape) 都会编译警告——逼迫你处理新情况。这是 ADT + Pattern Matching 的”编译期保证”。

如果没写 case null,编译器还会问”要不要处理 null”——比传统 switch 更友好。

六、模式匹配的优先级和顺序

switch 模式匹配的 case 是顺序匹配的——从上到下,第一个匹配的 case 生效。所以顺序很重要:

return switch (obj) {
    case Integer i when i > 100 -> "大整数";   // 1. 先匹配具体条件
    case Integer i -> "其他整数";               // 2. 再匹配类型兜底
    case Number n -> "其他数字";                // 3. 再匹配父类型
    default -> "非数字";                         // 4. 最后默认
};

如果反过来写 case Integer i 在前,i > 100 永远不会匹配——编译器会警告”case 永远不会执行”。

规则子类型在前,父类型在后;带 when 的在前,不带 when 的在后。

七、实战:重构复杂 if-else 链

下面的例子展示用 Pattern Matching 重构一个”消息处理器”——传统 if-else 链 vs switch 模式匹配的对比。

Java · 在线运行

观察重点

  • describeInstanceif instanceof——比传统少一行强转。
  • describeNumber 的 case 顺序——Integer i when i > 1000 必须在 Integer i 前,否则永远不匹配。
  • handleMessage 没有 default——Message 是 sealed,编译器知道只有 4 种。
  • describeShape 嵌套 Record 解构——Line(Point(...), Point(...)) 一行拆两层。
  • oldStyleDescribe vs newStyleDescribe——同样的逻辑,模式匹配版本代码量减半,可读性翻倍。
  • 流敏感作用域——if (o instanceof String s && s.length() > 2)s&& 右侧已绑定。

八、模式匹配的适用场景

场景例子
替代 if-else 类型分支消息分发、事件处理
解构 RecordAST 遍历、配置解析、JSON/SQL 解析树
处理 sealed ADT状态机、领域建模的”封闭枚举”
简化 equalsif (!(o instanceof X x)) return false;
替代 visitor 模式不用再写 Visitor 模式,switch 模式匹配更直接
优雅处理 nullcase null -> 显式处理

九、模式匹配的限制

当前 Java 21 的模式匹配还有一些限制(部分会在未来版本改进):

  • 不支持数组模式——case int[]{int a, int b, int rest} 还不能用(JEP 432 提案中)。
  • 不支持集合模式——case List(first, second, ...rest) 还不能用(未来的 JEP)。
  • when 不能引用 case 外的局部变量——如果会和外层变量冲突,编译错。
  • 不支持解构普通类——只有 Record 能解构,普通类不能用 case Foo(int x)(除非自定义解构方法,未来 JEP)。

但即便是当前版本,Pattern Matching 已经大幅提升了 Java 的表达力。

十、本章小结

概念核心要点
instanceof X x类型匹配 + 变量绑定,省去强转
流敏感作用域模式变量在”必然匹配”的路径上有效
switch 类型模式case String s ->,替代 if-else 链
switch Record 模式case Point(int x, int y) ->,解构 Record
when 守卫case Integer i when i > 0,附加条件
case null显式处理 null,不再 NPE
穷举检查sealed + switch 可省略 default
顺序敏感子类型在前,父类型在后;带 when 在前

记忆口诀

  • instanceof + 变量——判断即绑定,少写一行强转。
  • switch case 类型——if-else 链的终结者。
  • Record 模式解构——一行拆出组件。
  • when 加条件——类型 + 条件双重过滤。
  • case null 显式处理——告别 NPE。
  • sealed 配合——穷举检查,省 default。
  • 顺序敏感——具体在前,通用在后。

结语:Java 的现代数据建模三件套完成

到这里,现代 Java 数据建模三件套全部讲完了:

  • Records(第 47 章) —— 不可变值对象,积类型。
  • Sealed Classes(第 48 章) —— 封闭继承层次,和类型。
  • Pattern Matching(本章) —— 解构 + 穷举,让 ADT 真正可用。

这三者协同,让 Java 第一次能像 Scala、Haskell、Rust 一样优雅地建模代数数据类型。写一个 AST 解析器、状态机、领域模型,从过去的”Visitor 模式 + 一堆 if-else”变成”sealed + record + switch 模式匹配”——代码量减半,可读性翻倍,编译器还能帮你检查穷举。

下一章是第八阶段的收官——其他现代特性速览。我们一口气看完接口私有方法、Stream 增强、HttpClient、Text Blocks、序列化集合、FFM API 这些”小而美”的特性。它们没有三件套那么革命性,但每一个都在悄悄让 Java 更好用——我们下一章见。