← 返回题目列表

Java Record 是什么?它是否真正不可变?

高频 中等 第 8 / 24 题 更新于 2026/07/26
RecordJava 16数据载体不可变性

简化版

Record 是 Java 16 正式加入的透明数据载体,组件声明会生成对应的私有 final 字段、访问器、规范构造器以及基于组件的 equalshashCodetoString。它只保证组件引用不能重新赋值,也就是浅不可变;若组件指向可变集合,对象内容仍可被修改。

详细版

Record 类隐式为 final,直接继承 java.lang.Record,不能再继承其他类,也不能被继承;它可以实现接口、声明实例方法、静态成员和嵌套类型,但不能额外声明实例字段。组件访问器名是 name(),不是 JavaBean 风格的 getName()

规范构造器用于验证和归一化全部组件,紧凑构造器可省略参数列表及字段赋值,编译器会在构造器体结束后完成赋值。Record 适合值对象、DTO 和消息载体,但框架兼容性、敏感字段输出、可变组件与实体生命周期仍需单独评估。

完整版教学

一、Record 消除了哪些样板代码

public record Point(int x, int y) {}

这条声明给出两个 record components。编译器据此提供 private final 字段、x()y() 访问器、接收全部组件的规范构造器,以及 equalshashCodetoString 实现。

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 固定的是组件字段或引用,想让集合等内容也不可变,必须自行防御性复制。