摘要: 本文深入解析Java Record特性,从JDK 16正式引入的一行代码定义数据类开始,全面讲解Record的本质、使用场景、自定义构造方法、添加方法等核心用法。重点介绍JDK 21的Record Patterns解构功能,展示如何与密封类配合构建类型安全的代数数据类型。通过订单系统综合示例,演示Record在实际项目中的应用,帮助开发者掌握这一简化数据建模的现代Java特性。
关键词: Java Record, 数据类, Record Patterns, 密封类, 不可变对象
上一篇文章我们聊了异常处理。现在你的程序已经能从容地应对各种“意外情况”了,变得稳重了不少。
但如果你回过头来看我们写过的那些类,你可能会隐隐觉得——有些代码,是不是有点“啰嗦”?
比如我们写一个 Person 类,只有 name 和 age 两个字段,却要写构造方法、getter、toString()、equals()、hashCode()……少说二十行。如果项目里有几十个这样的类,光这些“样板代码”就能堆出好几千行。更别提每次新增一个字段,要改的地方有好几处,漏掉一个就可能引发 bug。
你可能会说:“我用 IDEA 的自动生成功能不就行了?”没错,工具能帮我们生成这些代码,但生成出来的代码还是要躺在文件里,占据你的屏幕空间,分散你的注意力。而且,如果两个地方对 equals() 的实现稍有不同,就可能出现难以排查的问题。
Java 的设计者也意识到了这个问题。从 JDK 14 开始预览、JDK 16 正式定型的 Record,就是为了解决这个痛点而生的。它让你用一行代码定义一个纯数据类,编译器替你生成所有那些“机械的”方法。
今天,我们就来深入认识 Record——它是什么、怎么用、有哪些高级玩法,以及它如何与 JDK 21 的其他新特性(如模式匹配、密封类)协同工作。
1. Record 是什么?—— 一行代码定义数据类
Record 是 Java 中一种特殊的类,它的核心使命是充当“透明数据载体”——只关心“装了什么数据”,不关心“怎么操作这些数据”。
用 record 关键字定义一个 Record,只需要在括号里列出它的组件(components)——也就是它要承载的数据字段:
public record Person(String name, int age) {}
就这一行,编译器会自动为你生成:
- 两个
private final字段:name和age。 - 一个全参构造方法:
Person(String name, int age)。 - 两个访问器方法:
name()和age()(注意:不叫getName()和getAge())。 toString()方法,输出格式为Person[name=张三, age=20]。equals()和hashCode()方法,基于所有组件实现。
对比一下传统写法:
// 传统写法:二十多行
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
// equals() 和 hashCode() 还要再写十几行...
}
// Record 写法:一行
public record Person(String name, int age) {}
是不是清爽了很多?
2. Record 的本质 —— 不可变的数据载体
Record 不仅仅是一种“简写语法”,它背后有一套严格的约束:
① 所有组件都是 final 的:Record 的字段一旦创建就不能修改。这让 Record 天然就是不可变对象,在多线程环境中非常安全。
② Record 本身是 final 的:你不能继承一个 Record,也不能被其他类继承。
public record Point(int x, int y) {}
// public class ColoredPoint extends Point {} // 编译错误!
③ 不能添加额外的实例字段:Record 的“数据清单”只能在头部声明,不能在主体里添加新的非静态字段。
public record User(String login) {
// int counter; // 错误!不能添加非静态字段
static int totalUsers = 0; // 可以,静态字段没问题
}
这些约束听起来像是“限制”,但其实它们正是 Record 的设计意图:明确地告诉读者和编译器——“这个类就是用来装数据的,没有任何隐藏状态”。这种透明性让 Record 在数据传递、序列化、模式匹配等场景中表现卓越。
3. Record 与普通类的选择
| 维度 | 普通类(Class) | Record |
|---|---|---|
| 可变性 | 任意(字段可 final 也可不) | 不可变:所有组件都是 final |
| 继承 | 可以继承其他类 | 不能继承,本身就是 final |
| 字段 | 任意字段 | 只能有组件(头部声明的字段),外加静态字段 |
| 访问器 | 手写 getter(可任意命名) | 自动生成,名与组件名相同(如 name()) |
equals/hashCode/toString | 手写或工具生成 | 自动生成,基于所有组件 |
| 构造方法 | 任意数量 | 一个主构造方法(涵盖所有组件),可添加紧凑构造器 |
| 适用场景 | 复杂业务逻辑、可变状态、需要继承 | 纯数据载体:DTO、方法多返回值、值对象 |
什么时候用 Record?
- 数据传输对象(DTO):从数据库查询返回的结果、API 交互的 JSON 对象。
- 方法的多个返回值:当一个方法需要返回多个值时,用 Record 封装比用数组或
Map更清晰。 - 值对象:坐标点、区间、货币金额等“由数据定义身份”的类型。
- 集合中的键:因为 Record 自动实现了正确的
equals()和hashCode(),非常适合作为HashMap的 key 或HashSet的元素。
什么时候用普通类?
- 对象有复杂的业务逻辑、可变状态、生命周期管理。
- 需要继承其他类来复用代码。
- 需要添加非静态的辅助字段(如缓存、计数器)。
一句话总结:如果你的类主要就是“装数据”,用 Record;如果它有“行为”和“状态变化”,用普通类。
4. 自定义构造方法 —— 让数据更“干净”
Record 自动生成的构造方法会把所有参数直接赋给字段。但很多时候,我们需要在对象创建时做一些校验或规范化——比如年龄不能为负、邮箱不能为空、坐标不能超出范围。
在 Record 中,你可以定义紧凑构造方法(Compact Constructor),它的参数列表被省略了,直接写 public RecordName { ... }:
public record Person(String name, int age) {
// 紧凑构造方法:参数自动与组件一致
public Person {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("姓名不能为空");
}
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年龄必须在 0~150 之间");
}
// 不需要手动写 this.name = name; this.age = age;
// 编译器会帮你完成赋值
}
}
紧凑构造方法的好处是:你只需要写校验逻辑,赋值的事情编译器帮你做。如果校验失败,对象根本不会被创建。
你也可以定义额外的构造方法,用 this(...) 调用主构造方法:
public record Range(int from, int to) {
// 额外构造方法:只传一个长度,从 0 开始
public Range(int size) {
this(0, size); // 调用主构造方法
}
}
这样你就可以用 new Range(10) 创建一个 from=0, to=10 的区间对象了。
5. 给 Record 添加方法 —— “数据”也可以有“行为”
Record 虽然主要是数据载体,但它可以拥有方法——包括实例方法和静态方法。这让 Record 可以在保持数据透明的同时,附带一些与数据紧密相关的操作。
public record Point(int x, int y) {
// 实例方法:计算到另一个点的距离
public double distanceTo(Point other) {
int dx = this.x - other.x;
int dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
// 静态方法:创建原点
public static Point origin() {
return new Point(0, 0);
}
}
使用起来很自然:
Point p1 = new Point(0, 0);
Point p2 = new Point(3, 4);
System.out.println(p1.distanceTo(p2)); // 5.0
System.out.println(Point.origin()); // Point[x=0, y=0]
💡 设计建议:Record 中的方法应该是与数据紧密相关的“派生计算”(比如
distanceTo由x和y算出),而不是复杂的业务逻辑。如果逻辑太复杂,建议放到独立的 Service 类中。
6. JDK 21 的 Record Patterns —— 解构的“魔法”
前面我们学了 Record 是“构造数据”的简洁方式。JDK 21 引入的 Record Patterns(记录模式) 则是“解构数据”的简洁方式。
在 Java 21 之前,如果你想从 Record 中取出数据,得先做类型检查、强制转换、再调用访问器——至少三四行代码:
record Point(int x, int y) {}
Object obj = new Point(5, 7);
if (obj instanceof Point) {
Point p = (Point) obj;
int x = p.x();
int y = p.y();
System.out.println("x=" + x + ", y=" + y);
}
Java 21 之后,Record Patterns 允许你在 instanceof 中直接解构 Record 的组件:
if (obj instanceof Point(int x, int y)) {
System.out.println("x=" + x + ", y=" + y); // 直接使用 x 和 y
}
一步到位:类型检查 + 解构 + 变量赋值,全部在一行完成。
嵌套解构
更强大的是,Record Patterns 可以嵌套。如果你的 Record 里还包含了其他 Record,你可以一次性把它们全部“拆开”:
record Size(int width, int height) {}
record Point(int x, int y) {}
record WindowFrame(Point origin, Size size) {}
Object obj = new WindowFrame(new Point(10, 20), new Size(800, 600));
// 嵌套解构:一步拿到所有数据
if (obj instanceof WindowFrame(Point(int x, int y), Size(int width, int height))) {
System.out.println("窗口左上角:(" + x + ", " + y + ")");
System.out.println("窗口大小:" + width + " x " + height);
}
如果没有 Record Patterns,你需要先转成 WindowFrame,再分别取 origin 和 size,再分别从它们里面取字段——代码会变得非常臃肿。
在 switch 中使用 Record Patterns
Record Patterns 在 switch 中同样大放异彩:
sealed interface Shape permits Point, Circle {}
record Point(int x, int y) implements Shape {}
record Circle(Point center, int radius) implements Shape {}
Shape shape = new Point(3, 4);
String description = switch (shape) {
case Point(int x, int y) -> "点在 (" + x + ", " + y + ")";
case Circle(Point(int cx, int cy), int r) ->
"圆心在 (" + cx + ", " + cy + "),半径 " + r;
};
System.out.println(description); // 点在 (3, 4)
注意这里 Circle 的匹配模式还嵌套了解构 Point——一切都是那么流畅和自然。
带守卫条件(Guard)
你还可以在 Record Pattern 后面加上 when 子句(或 && 条件),对匹配进一步筛选:
if (obj instanceof Point(int x, int y) && x == 0) {
System.out.println("点在 Y 轴上");
}
Record Patterns 让 Java 在处理结构化数据时,从“繁琐的类型检查和取值”变成了“优雅的模式匹配和解构”。这是 Java 迈向**面向数据编程(Data-Oriented Programming)**的重要一步。
7. Record 与密封类(Sealed Classes)的配合
JDK 17 引入了密封类(Sealed Classes),它允许你控制哪些类可以继承一个父类或实现一个接口。
Record 虽然不能继承普通类,但它可以实现接口。这意味着你可以用“密封接口 + Record 实现类”的组合,构建出封闭的、类型安全的代数数据类型(ADT)。
// 密封接口:只允许 Point、Circle、Rectangle 实现
sealed interface Shape permits Point, Circle, Rectangle {}
// 用 Record 实现密封接口
record Point(int x, int y) implements Shape {}
record Circle(Point center, int radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
// 使用时,switch 可以穷尽所有情况
double area = switch (shape) {
case Point(int x, int y) -> 0; // 点没有面积
case Circle(Point center, int r) -> Math.PI * r * r;
case Rectangle(double w, double h) -> w * h;
};
因为 Shape 是密封的,编译器知道所有可能的子类型就是 Point、Circle、Rectangle 这三个。如果你在 switch 中漏掉了任何一个,编译器会报错提醒你。这在处理复杂的数据结构时非常安全。
📌 Record 不能
extends其他类,但可以implements接口。所以“密封类 + Record”的正确组合方式是密封接口 + Record 实现类。
8. Record 的序列化
Record 天然支持序列化(只要实现 Serializable 接口),而且它的序列化机制比普通类更简单、更安全。
普通类的序列化会写入整个对象图,还可能涉及 readObject、writeObject 等自定义方法,容易出错。而 Record 的序列化只基于它的组件,反序列化时也只使用主构造方法。
import java.io.*;
public record User(String name, int age) implements Serializable {}
// 序列化和反序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.dat"))) {
oos.writeObject(new User("张三", 25));
}
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.dat"))) {
User user = (User) ois.readObject();
System.out.println(user); // User[name=张三, age=25]
}
⚠️ 注意:Record 不支持自定义序列化方法(如
writeObject、readObject),这是设计使然——保持简单和透明。如果需要更灵活的序列化,可以考虑 JSON 等格式。
9. Record 的最佳实践
✅ 推荐做法:
- 用 Record 表示值对象:坐标、金额、区间、邮箱地址等“由数据定义身份”的类型。
- 用 Record 作为 DTO:API 的请求/响应对象、数据库查询结果映射。
- 用 Record 作为方法的多个返回值:比
Object[]或Map类型安全得多。 - 在 Record 中做数据校验:用紧凑构造方法保证数据合法性。
- 给 Record 添加派生方法:比如
Point.distanceTo()、Range.contains()。 - 配合密封接口构建 ADT:用 Record 实现密封接口,配合 Record Patterns 做穷尽匹配。
❌ 避免的做法:
- 不要把 Record 当成“万能类”:如果有复杂业务逻辑、可变状态、需要继承,用普通类。
- 不要在 Record 中添加非静态字段:编译器不允许,而且违背了 Record 的设计初衷。
- 不要依赖
ordinal()做业务逻辑:这是给 Enum 用的,Record 没有这个概念。 - 不要在 Record 中塞入过多的组件:超过 5-6 个字段时,考虑是否真的需要这么多数据,或者是否可以拆分成更小的 Record。
10. 综合示例 —— 用 Record 重构订单系统
让我们把今天学的内容整合到一个完整的例子中:一个订单系统的核心数据模型,用 Record + 密封接口 + Record Patterns。
import java.time.LocalDateTime;
import java.util.List;
// ========== 密封接口:订单状态 ==========
sealed interface OrderStatus permits Pending, Paid, Shipped, Delivered, Cancelled {}
// 每个状态用 Record 表示,携带该状态特有的数据
record Pending(LocalDateTime createdAt) implements OrderStatus {}
record Paid(LocalDateTime paidAt, String transactionId) implements OrderStatus {}
record Shipped(LocalDateTime shippedAt, String trackingNumber) implements OrderStatus {}
record Delivered(LocalDateTime deliveredAt) implements OrderStatus {}
record Cancelled(LocalDateTime cancelledAt, String reason) implements OrderStatus {}
// ========== 订单项 Record ==========
record OrderItem(String productId, String productName, int quantity, double price) {
// 紧凑构造方法:校验数据
public OrderItem {
if (productId == null || productId.trim().isEmpty()) {
throw new IllegalArgumentException("商品ID不能为空");
}
if (quantity <= 0) {
throw new IllegalArgumentException("数量必须大于0");
}
if (price < 0) {
throw new IllegalArgumentException("价格不能为负数");
}
}
// 派生方法:计算小计
public double subtotal() {
return quantity * price;
}
}
// ========== 订单 Record ==========
public record Order(String orderId, String customerId, List<OrderItem> items,
OrderStatus status, LocalDateTime updatedAt) {
// 紧凑构造方法:校验
public Order {
if (orderId == null || orderId.trim().isEmpty()) {
throw new IllegalArgumentException("订单ID不能为空");
}
if (items == null || items.isEmpty()) {
throw new IllegalArgumentException("订单至少包含一个商品");
}
}
// 派生方法:计算订单总额
public double total() {
return items.stream()
.mapToDouble(OrderItem::subtotal)
.sum();
}
// 派生方法:判断订单是否可取消
public boolean isCancellable() {
return switch (status) {
case Pending _ -> true;
case Paid _ -> true;
case Shipped _ -> false;
case Delivered _ -> false;
case Cancelled _ -> false;
};
}
// 创建订单的工厂方法
public static Order create(String orderId, String customerId, List<OrderItem> items) {
return new Order(orderId, customerId, items,
new Pending(LocalDateTime.now()),
LocalDateTime.now());
}
// 状态变更方法(返回新订单,保持不可变)
public Order markAsPaid(String transactionId) {
return new Order(orderId, customerId, items,
new Paid(LocalDateTime.now(), transactionId),
LocalDateTime.now());
}
public Order markAsShipped(String trackingNumber) {
return new Order(orderId, customerId, items,
new Shipped(LocalDateTime.now(), trackingNumber),
LocalDateTime.now());
}
}
// ========== 主程序 ==========
public class Main {
public static void main(String[] args) {
// 1. 创建订单
var items = List.of(
new OrderItem("P001", "Java编程思想", 2, 99.0),
new OrderItem("P002", "设计模式", 1, 79.0)
);
Order order = Order.create("ORD-001", "CUST-001", items);
System.out.println("=== 新订单 ===");
System.out.println(order);
System.out.println("订单总额:" + order.total());
System.out.println("是否可取消:" + order.isCancellable());
// 2. 支付
Order paidOrder = order.markAsPaid("TXN-12345");
System.out.println("\n=== 支付后 ===");
System.out.println(paidOrder);
System.out.println("是否可取消:" + paidOrder.isCancellable());
// 3. 使用 Record Patterns 处理状态
System.out.println("\n=== 状态详情 ===");
String statusDetail = switch (paidOrder.status()) {
case Pending(var createdAt) -> "待支付,创建于 " + createdAt;
case Paid(var paidAt, var txnId) -> "已支付,交易号 " + txnId + ",支付时间 " + paidAt;
case Shipped(var shippedAt, var tracking) -> "已发货,运单号 " + tracking + ",发货时间 " + shippedAt;
case Delivered(var deliveredAt) -> "已送达,送达时间 " + deliveredAt;
case Cancelled(var cancelledAt, var reason) -> "已取消,原因:" + reason + ",取消时间 " + cancelledAt;
};
System.out.println(statusDetail);
}
}
这个例子展示了:
- Record 作为数据载体:
Order、OrderItem、以及各个状态类。 - 紧凑构造方法做校验:
OrderItem和Order都在创建时验证数据合法性。 - 派生方法:
total()、isCancellable()由数据计算得出。 - 不可变状态转换:
markAsPaid()、markAsShipped()返回新对象,不修改原对象。 - 密封接口 + Record 实现:
OrderStatus的所有可能状态都是 Record。 - Record Patterns:在
switch中解构状态对象,直接取出其中的数据。
11. 今天的总结
今天我们深入学习了 Record:
- Record 是什么:用一行代码定义不可变数据类的语法糖,自动生成构造方法、访问器、
toString、equals、hashCode。 - Record vs 普通类:Record 不可变、不能继承、只能有组件字段,适合纯数据载体。
- 紧凑构造方法:在 Record 内部做数据校验和规范化,编译器自动完成赋值。
- 给 Record 添加方法:实例方法和静态方法都可以,适合派生计算。
- Record Patterns(JDK 21):在
instanceof和switch中直接解构 Record 的组件,支持嵌套解构。 - Record + 密封接口:用 Record 实现密封接口,构建封闭的类型层次,配合 Record Patterns 实现穷尽匹配。
- 序列化:Record 天然支持序列化,机制简单安全。
- 最佳实践:用在 DTO、值对象、方法多返回值;复杂业务逻辑用普通类。
Record 是 Java 在“数据建模”领域的一次重要进化。它让“装数据”这件事变得前所未有的轻松,同时与模式匹配、密封类等新特性协同,为 Java 带来了更现代的编程范式。
动手试试
- 把你之前写的
Person、Student等类改写成 Record,感受代码量的变化。 - 定义一个
RangeRecord,包含start和end两个int组件。添加紧凑构造方法,确保start <= end。再添加一个contains(int value)方法,判断某个值是否在区间内。 - 定义一个密封接口
Result,用 Record 实现Success(携带数据T data)和Failure(携带错误信息String error)。写一个方法返回Result,在调用方用switch+ Record Patterns 处理两种情况。 - (挑战)结合第 16 篇的计算器项目,用 Record 重构计算结果。定义一个
CalculationResultRecord,包含expression、result、error三个组件,其中error可以为null。这样calculate方法可以直接返回CalculationResult,比之前用自定义类更简洁。
Record 的内容就讲到这里。下一篇文章,我们进入一个新的话题——包与导入。虽然我们一直在用 import,但它的细节和背后的模块化系统,值得专门聊一聊。
我们下一篇见。😃
📌 获取本系列示例代码请访问 GitCode。

225

被折叠的 条评论
为什么被折叠?



