Java 21 新特性实战:Record、Sealed、模式匹配

1. 引言

Java 21 作为 LTS(长期支持)版本,带来了多项重磅特性。其中,Record、Sealed Classes 和模式匹配(Pattern Matching)这三组特性相互配合,能够显著简化代码、提升可读性,并让类型系统表达力更强。本文将通过一个「订单状态处理」的实战案例,带你从零掌握这些新特性的核心用法与适用场景。

2. Record:不可变数据载体

2.1 什么是 Record

Record 是一种用于声明不可变数据载体的新类型。它自动生成构造器、equals()、hashCode()、toString() 以及访问器方法,极大减少了样板代码。

java 复制代码
// 传统写法
public class Order {
    private final String orderId;
    private final double amount;
    
    public Order(String orderId, double amount) {
        this.orderId = orderId;
        this.amount = amount;
    }
    
    public String getOrderId() { return orderId; }
    public double getAmount() { return amount; }
    // equals/hashCode/toString 省略...
}

// Record 写法
public record Order(String orderId, double amount) {}

2.2 Record 适合什么场景

  • 数据传输对象(DTO):用于在层与层之间传递数据,无需可变状态。
  • 不可变值对象:如坐标、金额、配置项等,天然适合不可变语义。
  • 多返回值:替代自定义的返回类,简洁表达一组相关数据。
  • 模式匹配的载体:与 switch 模式匹配结合,实现类型安全的解构。

注意:Record 的字段是 final 的,不可变。若需要可变状态,应使用普通类。

3. Sealed Classes:限制继承

3.1 什么是 Sealed Classes

Sealed Classes(密封类)允许你限制哪些类可以继承或实现它。通过 sealed 关键字声明,配合 permits 列出允许的子类型,使继承关系封闭且可控。

java 复制代码
public sealed interface OrderStatus permits Pending, Paid, Shipped, Cancelled {}

3.2 如何限制继承

java 复制代码
// 密封接口,只允许以下三个实现
public sealed interface OrderStatus 
    permits Pending, Paid, Cancelled {}

// 每个实现类必须声明为 final、sealed 或 non-sealed
public final class Pending implements OrderStatus {}
public final class Paid implements OrderStatus {}
public final class Cancelled implements OrderStatus {}

3.3 适用场景

  • 领域模型:当业务状态是有限且已知的集合时(如订单状态、支付状态)。
  • 增强安全性:防止外部代码随意扩展,保证类型层次完整。
  • 配合模式匹配:编译器能穷尽检查所有子类型,避免遗漏分支。

4. switch 模式匹配 + Record Patterns

4.1 switch 模式匹配

Java 21 中,switch 表达式支持类型模式匹配,可以直接对对象进行类型判断并绑定变量,替代传统的 instanceof + 强转写法。

java 复制代码
public static String describe(Object obj) {
    return switch (obj) {
        case Integer i -> "整数: " + i;
        case String s -> "字符串: " + s;
        case null -> "空值";
        default -> "未知类型";
    };
}

4.2 Record Patterns(记录模式)

Record Patterns 允许在模式匹配中直接解构 Record 的组件,一步完成类型判断和字段提取。

java 复制代码
public record Order(String orderId, double amount) {}

public static void printOrder(Object obj) {
    if (obj instanceof Order(String id, double amt)) {
        System.out.println("订单号: " + id + ", 金额: " + amt);
    }
}

4.3 嵌套模式匹配

Record Patterns 支持嵌套,可以匹配更复杂的结构。

java 复制代码
public record Customer(String name, Order order) {}

if (obj instanceof Customer(String name, Order(String id, double amt))) {
    System.out.println(name + " 的订单 " + id + " 金额为 " + amt);
}

5. 实战:用新特性重构订单状态处理

5.1 传统实现的问题

传统写法通常使用枚举 + 大量 if-else 或 switch 分支,代码冗长且容易遗漏状态。

java 复制代码
// 传统写法
public String handleOrder(Order order, OrderStatusEnum status) {
    if (status == OrderStatusEnum.PENDING) {
        return "订单待支付";
    } else if (status == OrderStatusEnum.PAID) {
        return "订单已支付,准备发货";
    } else if (status == OrderStatusEnum.SHIPPED) {
        return "订单已发货";
    } else {
        return "未知状态";
    }
}

5.2 使用 Record + Sealed + 模式匹配重构

首先定义密封的订单状态接口和 Record 实现:

java 复制代码
// 密封接口
public sealed interface OrderStatus permits Pending, Paid, Shipped, Cancelled {}

// 各状态用 Record 表示,携带各自的数据
public record Pending(String orderId) implements OrderStatus {}
public record Paid(String orderId, double amount) implements OrderStatus {}
public record Shipped(String orderId, String trackingNo) implements OrderStatus {}
public record Cancelled(String orderId, String reason) implements OrderStatus {}

然后使用 switch 模式匹配统一处理:

java 复制代码
public static String handleOrder(OrderStatus status) {
    return switch (status) {
        case Pending(String id) -> "订单 " + id + " 待支付";
        case Paid(String id, double amt) -> "订单 " + id + " 已支付 " + amt + " 元,准备发货";
        case Shipped(String id, String trackingNo) -> 
            "订单 " + id + " 已发货,运单号: " + trackingNo;
        case Cancelled(String id, String reason) -> 
            "订单 " + id + " 已取消,原因: " + reason;
    };
}

5.3 状态流转图

下面是订单状态的完整流转关系:
#mermaid-svg-aY0Od9KjbWJTo9aU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aY0Od9KjbWJTo9aU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aY0Od9KjbWJTo9aU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aY0Od9KjbWJTo9aU .error-icon{fill:#552222;}#mermaid-svg-aY0Od9KjbWJTo9aU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aY0Od9KjbWJTo9aU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aY0Od9KjbWJTo9aU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aY0Od9KjbWJTo9aU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aY0Od9KjbWJTo9aU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aY0Od9KjbWJTo9aU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aY0Od9KjbWJTo9aU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aY0Od9KjbWJTo9aU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aY0Od9KjbWJTo9aU .marker.cross{stroke:#333333;}#mermaid-svg-aY0Od9KjbWJTo9aU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aY0Od9KjbWJTo9aU p{margin:0;}#mermaid-svg-aY0Od9KjbWJTo9aU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-aY0Od9KjbWJTo9aU .cluster-label text{fill:#333;}#mermaid-svg-aY0Od9KjbWJTo9aU .cluster-label span{color:#333;}#mermaid-svg-aY0Od9KjbWJTo9aU .cluster-label span p{background-color:transparent;}#mermaid-svg-aY0Od9KjbWJTo9aU .label text,#mermaid-svg-aY0Od9KjbWJTo9aU span{fill:#333;color:#333;}#mermaid-svg-aY0Od9KjbWJTo9aU .node rect,#mermaid-svg-aY0Od9KjbWJTo9aU .node circle,#mermaid-svg-aY0Od9KjbWJTo9aU .node ellipse,#mermaid-svg-aY0Od9KjbWJTo9aU .node polygon,#mermaid-svg-aY0Od9KjbWJTo9aU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aY0Od9KjbWJTo9aU .rough-node .label text,#mermaid-svg-aY0Od9KjbWJTo9aU .node .label text,#mermaid-svg-aY0Od9KjbWJTo9aU .image-shape .label,#mermaid-svg-aY0Od9KjbWJTo9aU .icon-shape .label{text-anchor:middle;}#mermaid-svg-aY0Od9KjbWJTo9aU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aY0Od9KjbWJTo9aU .rough-node .label,#mermaid-svg-aY0Od9KjbWJTo9aU .node .label,#mermaid-svg-aY0Od9KjbWJTo9aU .image-shape .label,#mermaid-svg-aY0Od9KjbWJTo9aU .icon-shape .label{text-align:center;}#mermaid-svg-aY0Od9KjbWJTo9aU .node.clickable{cursor:pointer;}#mermaid-svg-aY0Od9KjbWJTo9aU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aY0Od9KjbWJTo9aU .arrowheadPath{fill:#333333;}#mermaid-svg-aY0Od9KjbWJTo9aU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aY0Od9KjbWJTo9aU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aY0Od9KjbWJTo9aU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aY0Od9KjbWJTo9aU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aY0Od9KjbWJTo9aU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aY0Od9KjbWJTo9aU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aY0Od9KjbWJTo9aU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aY0Od9KjbWJTo9aU .cluster text{fill:#333;}#mermaid-svg-aY0Od9KjbWJTo9aU .cluster span{color:#333;}#mermaid-svg-aY0Od9KjbWJTo9aU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aY0Od9KjbWJTo9aU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aY0Od9KjbWJTo9aU rect.text{fill:none;stroke-width:0;}#mermaid-svg-aY0Od9KjbWJTo9aU .icon-shape,#mermaid-svg-aY0Od9KjbWJTo9aU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aY0Od9KjbWJTo9aU .icon-shape p,#mermaid-svg-aY0Od9KjbWJTo9aU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aY0Od9KjbWJTo9aU .icon-shape .label rect,#mermaid-svg-aY0Od9KjbWJTo9aU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aY0Od9KjbWJTo9aU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aY0Od9KjbWJTo9aU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aY0Od9KjbWJTo9aU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户完成支付
仓库发货
用户取消/超时未支付
用户申请退款
物流异常/用户拒收
Pending 待支付
Paid 已支付
Shipped 已发货
Cancelled 已取消

各状态转换的触发条件如下:

  • Pending → Paid:用户完成支付,订单进入已支付状态,等待仓库发货。
  • Pending → Cancelled:用户主动取消订单,或超过支付时限系统自动取消。
  • Paid → Shipped:仓库完成拣货并发出包裹,生成运单号。
  • Paid → Cancelled:用户申请退款且商家同意,订单取消并原路退回款项。
  • Shipped → Cancelled:物流异常、用户拒收或退货,订单最终取消。

5.4 状态流转驱动代码

下面是一个完整的订单状态机实现,包含 pay()、ship()、cancel() 三个状态转换方法,并在非法转换时抛出异常:

java 复制代码
// 非法状态转换异常
public class IllegalStateTransitionException extends RuntimeException {
    public IllegalStateTransitionException(String message) {
        super(message);
    }
}

// 订单状态机
public class OrderStateMachine {
    private OrderStatus status;

    public OrderStateMachine(String orderId) {
        this.status = new Pending(orderId);
    }

    public OrderStatus getStatus() {
        return status;
    }

    /**
     * 支付订单:仅 Pending 状态可调用。
     * 业务含义:用户完成支付后,订单进入 Paid 状态并记录支付金额。
     */
    public void pay(double amount) {
        if (status instanceof Pending(String id)) {
            status = new Paid(id, amount);
        } else {
            throw new IllegalStateTransitionException(
                "当前状态 " + describe(status) + " 不能执行支付操作");
        }
    }

    /**
     * 发货:仅 Paid 状态可调用。
     * 业务含义:仓库完成拣货并发出包裹,生成运单号后进入 Shipped 状态。
     */
    public void ship(String trackingNo) {
        if (status instanceof Paid(String id, double amt)) {
            status = new Shipped(id, trackingNo);
        } else {
            throw new IllegalStateTransitionException(
                "当前状态 " + describe(status) + " 不能执行发货操作");
        }
    }

    /**
     * 取消订单:Pending、Paid、Shipped 状态均可调用。
     * 业务含义:用户取消、申请退款或物流异常时,订单进入 Cancelled 状态并记录原因。
     */
    public void cancel(String reason) {
        if (status instanceof Pending || status instanceof Paid || status instanceof Shipped) {
            String id = switch (status) {
                case Pending(String orderId) -> orderId;
                case Paid(String orderId, double amt) -> orderId;
                case Shipped(String orderId, String trackingNo) -> orderId;
                case Cancelled(String orderId, String r) -> orderId;
            };
            status = new Cancelled(id, reason);
        } else {
            throw new IllegalStateTransitionException(
                "当前状态 " + describe(status) + " 不能执行取消操作");
        }
    }

    // 复用 5.2 中的 switch 模式匹配描述方法
    private static String describe(OrderStatus status) {
        return switch (status) {
            case Pending(String id) -> "待支付";
            case Paid(String id, double amt) -> "已支付";
            case Shipped(String id, String trackingNo) -> "已发货";
            case Cancelled(String id, String reason) -> "已取消";
        };
    }
}

使用示例:

java 复制代码
OrderStateMachine order = new OrderStateMachine("ORD-001");
order.pay(99.9);                    // Pending -> Paid
order.ship("SF1234567890");         // Paid -> Shipped
order.cancel("物流异常");            // Shipped -> Cancelled

// 非法转换:已取消的订单再次支付会抛出异常
try {
    order.pay(99.9);
} catch (IllegalStateTransitionException e) {
    System.out.println(e.getMessage()); // 当前状态 已取消 不能执行支付操作
}

5.4 两种实现方式对比

下面从五个维度对比传统 if-else 实现与 Record + Sealed + 模式匹配实现的差异:

对比维度 传统 if-else 实现 Record + Sealed + 模式匹配
代码行数 每个状态分支需要独立的 if-else 判断,状态越多代码越长,且伴随大量强转和样板代码 每个状态用 Record 表达,switch 模式匹配一个分支对应一个状态,代码量显著减少
可读性 分支层层嵌套,业务逻辑被 if-else 结构淹没,需要仔细阅读才能理解状态流转 每个 case 分支清晰表达「状态 + 数据」的对应关系,业务意图一目了然
类型安全性 依赖枚举或字符串判断,编译器无法校验状态类型,运行时可能出现未知状态 密封接口限定状态集合,Record 携带类型化数据,编译器保证类型正确
扩展成本 新增状态需要修改多处 if-else 分支,容易遗漏,且遗漏时只能靠运行时兜底 新增状态只需实现密封接口并补充一个 case 分支,编译器会强制检查遗漏
编译期检查 无法在编译期发现遗漏的状态分支,未知状态只能靠 else 兜底 编译器穷尽检查所有密封子类型,遗漏分支直接编译报错,杜绝运行时意外

5.3 重构收益

  • 类型安全:编译器穷尽检查所有密封子类型,新增状态时强制处理。
  • 代码简洁 :消除了大量 if-else 和强转,逻辑一目了然。
  • 数据内聚:每个状态携带自己的数据,结构清晰。
  • 可维护性:新增订单状态只需实现接口并补充一个 case 分支。

5.5 常见问题与排查

在实际使用中,开发者常会遇到以下几类典型问题,下面逐一给出错误示例、正确示例与解决方案。

问题一:Record 的不可变性限制

Record 的字段默认是 final 的,无法在创建后修改。若试图在业务中直接修改字段,会编译失败。

java 复制代码
// 错误示例:Record 字段不可变,无法直接赋值
public record Order(String orderId, double amount) {}

Order order = new Order("ORD-001", 99.9);
order.amount = 199.9; // 编译错误:cannot assign a value to final variable amount

解决方案 :需要修改数据时,应使用 with 风格的方法返回新实例,或改用普通类承载可变状态。

java 复制代码
// 正确示例:通过 with 方法返回新实例,保持不可变语义
public record Order(String orderId, double amount) {
    public Order withAmount(double newAmount) {
        return new Order(orderId, newAmount);
    }
}

Order order = new Order("ORD-001", 99.9);
Order updated = order.withAmount(199.9); // 返回新对象,原对象不变

提示:若状态需要频繁原地修改,说明该对象不适合用 Record,应改用普通类。

问题二:Sealed 接口的 permits 遗漏编译错误

密封接口的 permits 必须列出所有直接子类型,且每个子类型必须与密封接口位于同一模块或同一包中。遗漏任一实现类都会导致编译错误。

java 复制代码
// 错误示例:permits 遗漏了 Shipped,编译报错
public sealed interface OrderStatus permits Pending, Paid, Cancelled {}

public record Pending(String orderId) implements OrderStatus {}
public record Paid(String orderId, double amount) implements OrderStatus {}
public record Shipped(String orderId, String trackingNo) implements OrderStatus {} // 编译错误:Shipped 不在 permits 列表中
public record Cancelled(String orderId, String reason) implements OrderStatus {}

解决方案 :确保 permits 完整列出所有直接子类型,并保持子类型与密封接口同包或同模块。

java 复制代码
// 正确示例:permits 完整列出全部四个子类型
public sealed interface OrderStatus permits Pending, Paid, Shipped, Cancelled {}

public record Pending(String orderId) implements OrderStatus {}
public record Paid(String orderId, double amount) implements OrderStatus {}
public record Shipped(String orderId, String trackingNo) implements OrderStatus {}
public record Cancelled(String orderId, String reason) implements OrderStatus {}

提示:新增状态时,务必同步更新 permits 列表,否则编译器会直接报错提醒你。

问题三:switch 模式匹配的穷尽性检查

对密封类型使用 switch 表达式时,编译器要求穷尽所有子类型分支。若遗漏某个分支,编译会直接失败。

java 复制代码
// 错误示例:遗漏了 Cancelled 分支,编译报错
public static String handleOrder(OrderStatus status) {
    return switch (status) {
        case Pending(String id) -> "订单 " + id + " 待支付";
        case Paid(String id, double amt) -> "订单 " + id + " 已支付 " + amt + " 元";
        case Shipped(String id, String trackingNo) -> "订单 " + id + " 已发货";
        // 缺少 case Cancelled -> 编译错误:switch expression does not cover all possible input values
    };
}

解决方案 :补全所有密封子类型分支,或添加 default 分支兜底。

java 复制代码
// 正确示例:补全所有分支,编译器穷尽检查通过
public static String handleOrder(OrderStatus status) {
    return switch (status) {
        case Pending(String id) -> "订单 " + id + " 待支付";
        case Paid(String id, double amt) -> "订单 " + id + " 已支付 " + amt + " 元";
        case Shipped(String id, String trackingNo) -> "订单 " + id + " 已发货";
        case Cancelled(String id, String reason) -> "订单 " + id + " 已取消,原因: " + reason;
    };
}

提示:穷尽性检查是密封类配合模式匹配的最大优势------新增状态时,编译器会强制你处理所有遗漏分支,从源头杜绝运行时意外。

6. 总结

特性 核心价值 适用场景
Record 简洁的不可变数据载体 DTO、值对象、多返回值
Sealed Classes 限制继承、封闭类型层次 领域模型、有限状态集合
模式匹配 类型安全的解构与分支 替代 instanceof、穷尽检查

三者结合,让 Java 代码在表达力、安全性和简洁性上迈上新台阶。建议在下一个项目中尝试用这些特性重构你的领域模型,体验函数式风格带来的清爽。

7. 延伸阅读

  • Java 官方文档:JEP 395(Record)、JEP 409(Sealed Classes)、JEP 441(模式匹配)
  • 尝试将本文的订单示例扩展为「支付状态机」,加深理解
  • 关注 Java 22+ 对模式匹配的进一步增强
相关推荐
霸道流氓气质1 小时前
LLM 应用限流与熔断机制完全指南:从多层防护架构到Java生产级弹性实战
java·开发语言·架构
大侠归来1 小时前
C语言内存管理:从栈到堆的完整指南
c语言·开发语言·python
m0_380743871 小时前
PHP7.0字符串在Docker怎么用
开发语言·php
不会就选b1 小时前
算法日常・每日刷题--<动态规划>1
java·数据结构·算法
晚安code1 小时前
JVM 内存结构入门:程序计数器、虚拟机栈、本地方法栈与堆的溢出诊断
java·jvm
自强的小白2 小时前
Spring事务失效的场景
java·spring
小蒜学长2 小时前
基于SpringBoot+Vue的小学数学智能出题系统(代码+数据库+LW)
java·数据库·spring boot·后端·智能出题系统
不灭的黄金瞳1232 小时前
Java继承与多态
java·intellij-idea
朝朝辞暮i2 小时前
C++ 第 10 课:函数 Function
开发语言·c++