Java Record 是 Java 16 正式转正的数据载体语法。当你只想用类来“装一组不可变数据”时,传统写法要手写构造器、getter、equals、hashCode、toString,样板代码又长又易错。Record 用一行 record Point(int x, int y) {} 自动生成这些成员,让“不可变数据类”成为 Java 的一等公民。
一、为什么需要 Record
一个“点坐标”“一笔交易”“一个配置项”这类纯数据对象,在 Java 里往往要写一大段样板:私有字段、全套 getter、构造器、equals、hashCode、toString。写起来枯燥,equals/hashCode 一旦手写错还会引发隐蔽 bug。本质上我们只关心“它有哪些数据”,而不是“怎么管这些数据”。
Java 16 把 Record 正式转正(从 JDK 14 起预览),专门解决“不可变数据载体”这件事。声明即契约,编译器替你生成全部样板。它和 Java 注解处理器驱动的 Lombok @Data 思路相似,但 Record 是语言原生能力,零依赖、零注解处理器。
二、Record 基础语法与自动生成成员
声明一个 Record 极其简单,组件(component)写在括号里:
// 声明即定义:x、y 是组件
public record Point(int x, int y) {
// 无需手写任何字段与方法
}
编译器会为它自动生成:每个组件对应的私有 final 字段、与组件同名且无 get 前缀的访问器、一个全参数公有构造器,以及基于所有组件的 equals、hashCode 和按组件顺序的 toString。注意访问器叫 x()、y(),而不是 JavaBean 风格的 getX()。
2.1 自动生成的成员对照
| 成员 | 生成规则 |
| 字段 | 每个组件对应一个 private final 字段 |
| 访问器 | 与组件同名方法 x(),无 get 前缀 |
| 构造器 | 全参数公有构造器 Point(int x, int y) |
| equals/hashCode | 基于全部组件,语义清晰稳定 |
| toString | 形如 Point[x=1, y=2] |
三、紧凑构造器做参数校验
Record 的构造器有一个特殊形态——紧凑构造器(compact constructor)。它没有参数列表,直接作用于隐式参数,最适合做参数校验与规范化:
public record Range(int lo, int hi) {
// 紧凑构造器:无参数列表,直接校验组件
public Range {
if (lo > hi) {
throw new IllegalArgumentException("lo 不能大于 hi: " + lo + "," + hi);
}
}
}
注意:紧凑构造器里不能重新声明组件字段,也不能给 this 赋值,只能做前置检查,或把规范化后的值赋给同名的局部变量(Java 会把局部变量值写回组件)。这种方式保证了“对象一旦创建,约束必然成立”。
四、自定义构造器与委托
当需要一个额外入口(例如从“元”构造“分”)时,自定义构造器必须首先显式委托给规范构造器,这正是 Java 保证不可变性的关键约束:
public record Money(long cents) {
// 自定义构造器:先委托规范构造器
public Money(double yuan) {
this(Math.round(yuan * 100));
}
}
Money m = new Money(19.9); // 内部转为 1990 分
System.out.println(m.cents()); // 1990
五、不可变性的边界
Record 的组件字段都是 final,创建后引用不能改。但这不等于“完全不可变”:如果某个组件是 List、数组或其他可变对象,容器内部的元素仍然可以被修改。
public record Bag(List<Integer> items) {}
var bag = new Bag(new ArrayList<>(List.of(1, 2)));
bag.items().add(3); // 危险!外部仍能修改内部列表
System.out.println(bag.items()); // [1, 2, 3]
对策:在构造器里做防御性拷贝,或只放不可变类型。例如用 List.copyOf(items) 把入参转成不可变列表,再赋值给组件,彻底堵住后门。这也是《Effective Java》里反复强调的“可变内部状态要拷贝”原则在 Record 上的具体落地。
六、实现接口、泛型与嵌套
Record 可以实现接口、携带泛型,也能在内部嵌套,非常适合表达结构化数据:
// 泛型 + 实现接口
public record Pair<T>(T first, T second) implements Comparable<Pair<T>> {
public int compareTo(Pair<T> o) {
return ((Comparable<T>) first).compareTo(o.first);
}
}
// 嵌套 Record 表达订单与明细
public record Order(long id, List<Item> items) {
public record Item(long sku, int qty) {}
}
这里的泛型机制与 Java 泛型通配符的类型擦除 一脉相承;而嵌套 Record 常用于把“子结构”声明为静态成员,避免零散的 POJO 满天飞,也让 JSON 序列化时的层级结构一目了然。
七、与 JSON 框架、反射的配合
主流 JSON 库早已支持 Record:Jackson 2.12+、Gson 都能直接反序列化。Record 天然契合序列化场景(DTO、配置、接口返回值):
ObjectMapper mapper = new ObjectMapper();
Point p = mapper.readValue("{"x":3,"y":4}", Point.class);
String json = mapper.writeValueAsString(p); // {"x":3,"y":4}
通过 Java 反射 读取组件元数据也很方便,常用于通用序列化、校验或测试框架:
for (var c : Point.class.getRecordComponents()) {
System.out.println(c.getName() + " -> " + c.getType());
}
// 输出:x -> int
// y -> int
八、Record 与 Lombok @Data 的差异
两者都能减少样板,但定位不同。Record 是“不可变数据载体”的官方答案,Lombok 是“可定制样板生成器”:
| 维度 | Java Record | Lombok @Data |
| 不可变性 | 字段 final,天然不可变 | 默认可变,需 @Value 才不可变 |
| 依赖 | 语言原生,零依赖 | 需引入注解处理器 |
| equals/hashCode | 基于所有组件,语义固定 | 可定制,但易踩坑 |
| 继承 | 隐式 final,不能继承/被继承 | 普通类,可继承 |
| 适用场景 | 纯数据载体、DTO | 需要行为/可变性/混合的实体 |
九、常见坑与排障
| 坑 | 对策 |
| 想给 Record 加可变 setter | 不要加;换普通类或新建 Record |
| 组件过多(>5 个) | 拆成更小的值对象,提升可读性 |
| 组件是可变容器(List/数组) | 构造器里 List.copyOf 防御性拷贝 |
| 老版本 JSON 库不识别 | 升级 Jackson/Gson 或注册 Record 模块 |
| 与 JPA Entity 混用 | 不建议;Entity 需要可变与无参构造 |
十、落地建议:何时该用 Record
结合 Java 函数式编程的不可变思想,Record 在以下场景是首选:
- 优先用 Record:方法返回值、DTO、配置项、事件/消息载体、模式匹配(Java 21 switch 模式)的判别联合。
- 用普通类:JPA Entity、需要 setter 可变状态、含复杂业务行为的领域对象。
- 组合使用:Record 与 Stream 收集、嵌套结构配合极佳,能把“数据形状”表达得非常干净。
十一、Record 与 sealed:表达判别联合
Java 17 的 sealed 接口与 Record 是表达“判别联合(discriminated union)/代数数据类型”的黄金组合。例如一个表达式树或消息类型,用 sealed interface 约束子类型,再用 Record 实现每个分支,配合 Java 21 的 switch 模式匹配就能写出穷尽且安全的分支处理:
public sealed interface Expr permits Expr.Num, Expr.Add, Expr.Mul {
record Num(int v) implements Expr {}
record Add(Expr l, Expr r) implements Expr {}
record Mul(Expr l, Expr r) implements Expr {}
}
// Java 21:switch 模式匹配,编译器保证 case 穷尽
int eval(Expr e) {
return switch (e) {
case Expr.Num n -> n.v();
case Expr.Add a -> eval(a.l()) + eval(a.r());
case Expr.Mul m -> eval(m.l()) * eval(m.r());
};
}
这种写法把“可能的值”写进了类型系统,编译器保证 switch 的 case 穷尽;将来新增一个分支却忘了处理,会直接在编译期报错,而不是运行时遗漏。它比用 Object 加 instanceof 判断安全得多,也更易于阅读。
十二、性能与内存:Record 真的更省吗
Record 在字节码层面就是一个普通的 final 类,没有任何隐藏开销;它的 equals、hashCode 是编译器生成的普通方法,运行性能与手写版本等价。内存占用上,Record 并不会比等价的普通 POJO 更小——它省下的是“写样板的人肉成本”,而不是对象的字节数。
真正的收益在正确性与可维护性:不可变 + 值语义让对象在并发、缓存、集合键(HashMap key)场景下更安全,不会因为被意外修改而破坏 equals/hashCode 契约。如果你关心对象头、字段对齐与内存布局,应当去看 JVM 对象布局工具(JOL),而不是指望 Record 本身带来压缩。
十三、30 秒上手清单
- 声明:
record T(Type a, Type b) {},编译器自动生成全部样板。 - 访问器叫
a()不是getA();想校验就用紧凑构造器。 - 不变性有边界:组件是 List/数组时要防御性拷贝。
- DTO/返回值用 Record,可变实体用普通类。
- 序列化直接交给 Jackson/Gson,无需额外配置(新版)。
借助 Docker Compose 起一个本地 Java 21 环境,把上面的片段跑一遍,你就能直观感受到 Record 如何把“样板地狱”变成“一行声明”,并结合 sealed 与模式匹配写出既安全又优雅的代码。




