Java Record 是什么?它是否真正不可变?
简化版
Record 是 Java 16 正式加入的透明数据载体,组件声明会生成对应的私有 final 字段、访问器、规范构造器以及基于组件的 equals、hashCode、toString。它只保证组件引用不能重新赋值,也就是浅不可变;若组件指向可变集合,对象内容仍可被修改。
详细版
Record 类隐式为 final,直接继承 java.lang.Record,不能再继承其他类,也不能被继承;它可以实现接口、声明实例方法、静态成员和嵌套类型,但不能额外声明实例字段。组件访问器名是 name(),不是 JavaBean 风格的 getName()。
规范构造器用于验证和归一化全部组件,紧凑构造器可省略参数列表及字段赋值,编译器会在构造器体结束后完成赋值。Record 适合值对象、DTO 和消息载体,但框架兼容性、敏感字段输出、可变组件与实体生命周期仍需单独评估。
完整版教学
一、Record 消除了哪些样板代码
public record Point(int x, int y) {}
这条声明给出两个 record components。编译器据此提供 private final 字段、x() 和 y() 访问器、接收全部组件的规范构造器,以及 equals、hashCode、toString 实现。
Record 的语义不仅是少写代码,而是类的 API 和状态都由组件列表定义。相等性比较要求对象属于同一个 Record 类,并按所有组件值比较。
二、如何校验与归一化数据
紧凑构造器适合统一校验组件:
public record User(String name, List<String> roles) {
public User {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("name 不能为空");
}
name = name.trim();
roles = List.copyOf(roles);
}
}
紧凑构造器体中操作的是构造参数,不能显式给组件字段赋值;编译器会在构造器体正常结束后把最终参数值赋给字段。这里用 List.copyOf 做防御性复制,避免调用方继续修改原集合。
三、为什么只是浅不可变
组件字段是 final,只说明记录中的引用不能改指向,并不让被引用对象自动变成不可变对象:
record Team(List<String> members) {}
List<String> members = new ArrayList<>();
Team team = new Team(members);
members.add("Alice"); // team.members() 也能看到变化
若业务要求值对象稳定,应在构造时复制可变参数,并避免访问器暴露可变内部状态。数组同样需要特别处理,因为默认相等性对数组组件采用数组对象自身的 equals 语义,不会自动进行深层元素比较。
四、Record 能做与不能做的事
Record 可以实现接口、重写组件访问器、声明普通实例方法和静态工厂。自定义规范构造器与访问器时,不能破坏组件所声明的类型契约。
Record 不能声明额外实例字段,不能显式继承其他类,也不能作为其他类的父类。它不是传统 JavaBean:访问器没有 get 前缀,也没有无参构造器和 setter,因此依赖这些约定的旧框架可能需要适配。
五、适用边界
Record 很适合 API 请求响应、领域值对象、事件消息和查询投影,因为这些类型的核心就是一组明确的数据。需要可变生命周期、延迟加载、代理子类或复杂身份语义的持久化实体,未必适合直接改成 Record。
自动生成的 toString() 会包含组件名和值,若组件含密码、令牌等敏感信息,需要重写输出或避免把敏感值作为组件。减少样板代码不能替代数据安全和对象建模判断。
六、相等性、数组组件与防御性复制
自动生成的 equals 按组件逐一比较,但数组的 equals 仍是数组对象自身的相等语义,不会自动调用 Arrays.equals。两个 record Bytes(byte[] data) 即使数组内容同为 [1, 2],只要是两个不同数组,默认 record 实例通常就不相等;若业务需要内容值语义,应重新设计组件类型或谨慎重写 equals/hashCode。
| 组件类型 | final 保证 | 默认 equals 效果 | 深不可变需要什么 |
|---|---|---|---|
int | 数值固定 | 按值 | 已满足 |
String | 引用固定 | String 值比较 | String 本身不可变 |
List<String> | 引用固定 | List 元素比较 | 构造时 List.copyOf |
byte[] | 数组引用固定 | 默认数组身份比较 | 复制且定义内容相等性 |
假设构造 Record 时传入 1,000 个元素的 ArrayList,List.copyOf 会产生一次 O(n) 复制,但换来对象不受调用者后续修改影响。如果对象会长期缓存或作为 Map key,这个成本通常比值在入表后悄悄变化更可控;性能敏感时也应基于所有权设计,而不是省掉复制后共享可变集合。
调用方可变集合 ──防御性复制──> Record 内部不可变列表
调用方后续 add/remove 不再改变 Record 的值
心法:Record 自动生成的是“组件驱动的值外形”,不是“组件对象的深不可变性”。
七、常见误区与追问
- 误区:Record 的所有组件都会自动深拷贝。 编译器只保存传入值,集合和数组仍需显式防御性复制。
- 误区:Record 就是带 getter/setter 的 JavaBean。 访问器名是组件名且没有 setter,也不天然提供无参构造器。
- 误区:Record 可以继承业务父类复用状态。 它隐式继承
java.lang.Record,不能再显式继承其他类,但可以实现接口。 - 误区:数组组件会自动按元素参与 equals。 数组默认 equals 基于身份,需要单独处理内容值语义。
- 追问:紧凑构造器何时给字段赋值? 构造器体正常结束后由编译器把最终参数值赋给组件字段,体内可校验和归一化参数。
- 追问:Record 能否声明额外实例字段? 不能;可以声明静态字段、实例方法、静态方法和嵌套类型。
- 追问:为什么敏感字段要警惕自动 toString? 生成的输出包含组件名和值,日志中可能泄露令牌或密码。
八、加强记忆
Record 用组件列表同时声明状态和 API,自动提供构造、访问、相等性与字符串表示;它隐式 final、不能增加实例字段。最容易答错的是不可变性:final 固定的是组件字段或引用,想让集合等内容也不可变,必须自行防御性复制。