Java Records:不可变数据载体
什么是 Java Record
Java Record 是一种特殊的类,从 Java 16 开始正式成为语言标准。它专门用于充当不可变的数据载体,旨在减少样板代码,让开发者用一行声明就能定义出只包含数据的类。
传统上,要创建一个简单的数据类(如 DTO、值对象),你需要手动编写构造器、equals()、hashCode()、toString()以及各字段的 getter,这些代码冗长且容易出错。Record 可以自动生成这些方法,让你专注在数据本身。
快速入门:创建你的第一个 Record
Record 使用 record 关键字声明,后面紧跟名称和括号内的组件列表(components)。组件的类型和名称一起定义了该 Record 的状态。
public record Point(int x, int y) { }
以上一行代码就完成了传统需要几十行的 Point 类。编译器会自动生成:
x()和y()两个访问器方法(而不是getX()、getY())- 一个规范构造器
Point(int x, int y) equals(Object o)、hashCode()和toString()方法
使用起来也像普通类一样:
Point p = new Point(3, 4);
System.out.println(p.x()); // 3
System.out.println(p); // Point[x=3, y=4]
自动生成的方法详解
访问器(Accessor)
Record 为每个组件生成一个与组件同名的公共方法,没有 get 前缀。例如 x()、y()。这是 Java 命名惯例的简化,体现了“数据即方法”的思维。
规范构造器
Record 自动生成的构造器会接收全部组件作为参数,并顺序赋值。你可以在不声明构造器的情况下直接使用:
var p = new Point(5, 10);
equals() 与 hashCode()
自动生成的 equals() 方法会基于所有组件进行比较,只要两个 Record 实例的对应组件值相等,它们就相等。hashCode() 也基于所有组件生成,保证一致性。
toString()
返回格式固定为 ClassName[component1=value1, component2=value2, ...],非常适合日志调试。
自定义 Record 行为
Record 并不只是“只读数据壳”,你也可以添加自定义的方法和行为,但必须遵守不可变性原则。
紧凑构造器(Compact Constructor)
你可以对组件进行验证或规范化处理,而无需重新声明参数列表。这种构造器没有参数括号,直接访问组件:
public record Range(int start, int end) {
public Range {
if (start > end) {
int temp = start;
start = end;
end = temp;
}
// 这里自动将参数赋值给字段
}
}
紧凑构造器中的代码在自动生成的赋值操作之前执行。赋给 start、end 实际上是在修改参数值,最终这些值会被赋给底层字段。
重写规范构造器
你也可以写完整的规范构造器,但必须手动赋值给所有组件:
public record Range(int start, int end) {
public Range(int start, int end) {
if (start > end) throw new IllegalArgumentException("start must be <= end");
this.start = start;
this.end = end;
}
}
添加静态字段与方法
你可以像普通类一样添加静态字段、静态方法或泛型方法:
public record Circle(double radius) {
public static final double PI = 3.1415926;
public double area() {
return PI * radius * radius;
}
public static Circle fromDiameter(double diameter) {
return new Circle(diameter / 2);
}
}
实例方法可以任意访问组件,但不能修改它们(因为字段是 final 的)。
不可变性的保障
Record 的所有组件字段都是 private final,没有 setter 方法,保证了实例一旦创建就无法被改变。这天然适合作为值对象、缓存键、多线程共享数据。
Record 的限制与特性
为了保持“纯粹的不可变数据载体”定位,Record 有以下限制:
- 隐式 final:Record 是隐式的最终类,不能被其他类继承。
- 不能显式继承其他类:每个 Record 都隐式继承
java.lang.Record,因此不能再继承其他类。 - 可以实现接口:Record 可以像普通类一样实现任意接口。
- 不能声明非静态字段:实例字段只能是组件列表中的字段,不允许额外定义实例变量(但静态变量可以)。
- 构造器必须调用其他构造器或最终赋值全部组件:无论你加了什么构造器,最终都需要确保所有组件字段被赋值。
- 没有默认构造器:因为组件列表定义了全部状态,不存在无参构造器。
这些限制确保了 Record 在任何场景下都保持语义明确、行为可预测。
何时应该使用 Record?
- 数据传输对象(DTO):在不同层之间传递纯数据。
- 值对象:坐标、货币金额、范围、颜色等由几个值唯一确定的对象。
- 返回多个值:方法需要返回多个同类型结果时,可以用 Record 包装(替代 Pair/Tuple)。
- 配置或不可变快照:保存某一时刻的状态。
- 作为 Map 的键:因为
equals/hashCode基于值自动生成,非常适合作为键。
不适合的场景:
- 需要可变字段的实体类(如 JPA 实体,除非项目框架支持 Record)。
- 需要复杂的继承层次。
- 字段不固定、需要动态添加属性的容器。
与传统类及 Lombok 的对比
| 特性 | 传统 POJO | Lombok @Value | Record |
|---|---|---|---|
| 代码量 | 很多(构造器、getter、equals等) | 一行注解 | 一行声明 |
| 不可变性 | 需手动保证 | 自动将所有字段设为 private final | 自动 final 字段,无 setter |
| 继承 | 可继承其他类 | 可继承其他类 | 默认 final,不能继承类 |
| 字段命名风格 | getX() |
getX() |
x() |
| 语言支持 | 纯 Java | 依赖第三方库 | 原生 Java,无额外依赖 |
Record 是 Java 语言层面的官方解决方案,比 Lombok 更加轻量和标准化,没有字节码黑魔法,调试起来更直观。
进阶:Record 中的泛型与嵌套
Record 完美支持泛型,可以创建可复用的数据容器:
public record Pair<K, V>(K key, V value) { }
Pair<String, Integer> pair = new Pair<>("age", 30);
Record 内部也可以嵌套其他 Record:
public record Address(String city, String street) { }
public record Customer(String name, Address address) { }
总结
Java Record 是编写不可变数据类的最佳实践方式。它通过简明的语法消除冗余代码,内建标准化语义,让开发者更关注数据本身的设计与业务逻辑。对于任何表示“纯数据”的场景,优先考虑使用 Record,你的代码将更加简洁、安全且易于维护。