record 数据载体
学习目标
- 用 record 声明字段固定、主要用于承载数据的值对象。
- 会写规范构造器和紧凑构造器,建立输入校验与规范化规则。
- 分清 record 自动生成的成员、浅不可变边界和 JDK 20 的 record patterns。
核心知识点
专业术语
- record component:record User(String name, int age) 括号中的组件声明。
- 规范构造器(canonical constructor):参数列表与组件完全对应的构造器。
- 紧凑构造器(compact constructor):省略参数列表的规范构造器写法,可以在隐式字段赋值前校验参数。
- 浅不可变:组件引用不能重新赋值,但引用指向的可变对象仍可能变化。
- record pattern:把 record 的组件直接绑定到模式变量的模式语法;JDK 20 中属于预览特性。
白话解释与边界
record 解决“这几个字段共同表示一个值”的问题,自动生成访问器、equals、hashCode 和 toString,并不替你完成深复制、数据库映射或业务生命周期管理。组件字段是 private final,但 record Order(List<String> items) 仍可能因为列表本身可变而被外部修改。需要隔离集合结构时,在构造器中使用 List.copyOf;需要深不可变时,还要让元素本身不可变或逐个复制。
record 不能继承普通类,只能隐式继承 java.lang.Record,但可以实现接口。record 的访问器名称是组件名(user.name()),不是 JavaBean 风格的 getName()。
常用用法
声明最小 record:自动获得值语义成员
需要声明小型值对象时使用 record 组件,编译器会生成同名访问器、规范构造器以及 equals、hashCode 和 toString。
// 语义:需要声明小型值对象时使用 record 组件,编译器会生成同名访问器、规范构造器以及 equals、hashCode 和 toString。
// 初始状态:left 初始为 new Point(3, 4);right 初始为 new Point(3, 4)。
record Point(int x, int y) {}
public class RecordBasicDemo {
public static void main(String[] args) {
Point left = new Point(3, 4);
// 关键变化:left 接收表达式 new Point(3, 4) 的计算结果。
// 初始状态:left 的初始值为 new Point(3, 4)。
Point right = new Point(3, 4);
// 初始状态:right 也保存 x=3、y=4,用于与 left 比较。
System.out.println(left.x() + ", " + left.y());
// 输出:3, 4
System.out.println(left.equals(right) + ", " + left);
// 输出:true, Point[x=3, y=4]
}
}使用紧凑构造器校验和规范化参数
紧凑构造器的参数名就是组件名,可以在隐式赋值前校验和规范化;不要在其中再次给字段赋值,record 组件字段由编译器完成赋值。
// 语义:紧凑构造器的参数名就是组件名,可以在隐式赋值前校验和规范化。
// 初始状态:name 初始为 new UserName(" Alice ")。
record UserName(String value) {
public UserName {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("name must not be blank");
// 异常:throw new IllegalArgumentException("name must not be blank") 立即进入异常路径。
// 初始状态:表达式为 throw new IllegalArgumentException("name must not be blank")。
}
value = value.trim();
}
}
public class RecordCompactConstructorDemo {
public static void main(String[] args) {
UserName name = new UserName(" Alice ");
// 初始状态:构造器去除两端空白后,name.value() 为 "Alice"。
System.out.println(name.value());
// 输出:Alice
}
}写显式规范构造器:需要清晰控制赋值时使用
显式规范构造器的参数必须与组件一一对应,并且要明确给每个组件赋值;大多数校验场景用紧凑构造器更简洁。
// 语义:显式规范构造器的参数必须与组件一一对应,并且要明确给每个组件赋值。
record Port(int value) {
public Port(int value) {
if (value < 1 || value > 65535) {
throw new IllegalArgumentException("port out of range");
// 异常:throw new IllegalArgumentException("port out of range") 立即进入异常路径。
// 初始状态:表达式为 throw new IllegalArgumentException("port out of range")。
}
this.value = value;
}
}
public class RecordCanonicalConstructorDemo {
public static void main(String[] args) {
Port port = new Port(8080);
// 初始状态:port.value() 通过范围校验并保存 8080。
System.out.println(port.value());
// 输出:8080
}
}用组件访问器读取数据
访问器名就是组件名;如果框架要求 getName(),可以额外定义方法,但不要误以为 record 自动生成 JavaBean getter。
// 语义:访问器名就是组件名。
// 初始状态:user 初始为 new User("Alice", 20)。
record User(String name, int age) {}
public class RecordAccessorDemo {
public static void main(String[] args) {
User user = new User("Alice", 20);
// 关键变化:user 接收表达式 new User("Alice", 20) 的计算结果。
// 初始状态:user 的初始值为 new User("Alice", 20)。
System.out.println(user.name() + ", " + user.age());
// 输出:Alice, 20
}
}用 List.copyOf 隔离可变集合组件
需要隔离 record 的可变列表组件时使用 List.copyOf,它会复制列表结构并拒绝 null,但不会复制可变元素本身。
// 语义:需要隔离 record 的可变列表组件时使用 List.copyOf,它会复制列表结构并拒绝 null,但不会复制可变元素本身。
// 初始状态:source 初始为 new ArrayList<>(List.of("book"));order 初始为 new Order(source)。
import java.util.ArrayList;
import java.util.List;
record Order(List<String> items) {
public Order {
items = List.copyOf(items);
}
}
public class RecordShallowImmutableDemo {
public static void main(String[] args) {
List<String> source = new ArrayList<>(List.of("book"));
// 关键变化:source 接收表达式 new ArrayList<>(List.of("book")) 的计算结果。
// 初始状态:source 的初始值为 new ArrayList<>(List.of("book"))。
Order order = new Order(source);
// 返回:new Order(source) 通过 List.copyOf 复制列表结构,order.items() 初始为 [book]。
source.add("pen");
// 关键变化:source.add("pen") 只修改外部 source,source 变为 [book, pen],order.items() 仍为 [book]。
System.out.println(order.items());
// 输出:[book]
}
}让 record 实现接口:统一值对象能力
record 可以实现一个或多个接口,适合让不同数据载体遵守同一读取契约;它不能通过 extends SomeClass 继承普通类。
// 语义:record 可以实现一个或多个接口,适合让不同数据载体遵守同一读取契约。
interface Identified {
String id();
}
record Product(String id, String name) implements Identified {}
public class RecordInterfaceDemo {
static String show(Identified value) {
return value.id();
}
public static void main(String[] args) {
Product product = new Product("p-1", "Book");
// 初始状态:product.id() 为 "p-1",product.name() 为 "Book"。
System.out.println(show(product));
// 输出:p-1
}
}使用 record pattern 直接拆出组件(JDK 20 预览)
需要在 JDK 20 中合并类型判断、转换和组件读取时可使用预览版 record pattern,并为编译与运行配套启用预览特性。
// 语义:需要在 JDK 20 中合并类型判断、转换和组件读取时可使用预览版 record pattern,并为编译与运行配套启用预览特性。
record Point(int x, int y) {}
public class RecordPatternDemo {
static String locate(Object value) {
if (value instanceof Point(int x, int y)) {
return "x=" + x + ", y=" + y;
}
return "unknown";
}
public static void main(String[] args) {
Point point = new Point(2, 5);
// 初始状态:point.x() 为 2,point.y() 为 5。
System.out.println(locate(point));
// 输出:x=2, y=5
}
}javac --release 20 --enable-preview RecordPatternDemo.java
java --enable-preview RecordPatternDemo使用嵌套 record pattern 读取嵌套数据
嵌套模式适合小型、结构稳定的数据树;当校验逻辑复杂或需要多处复用时,先显式转换为局部变量通常更容易调试。
// 语义:javac --release 20 --enable-preview RecordPatternDemo.java java --enable-preview RecordPatternDemo。
record Address(String city) {}
record User(String name, Address address) {}
public class NestedRecordPatternDemo {
static String cityOf(Object value) {
if (value instanceof User(String name, Address(String city))) {
return name + "@" + city;
}
return "unknown";
}
public static void main(String[] args) {
User user = new User("Alice", new Address("Shanghai"));
// 初始状态:user.name() 为 "Alice",user.address().city() 为 "Shanghai"。
System.out.println(cityOf(user));
// 输出:Alice@Shanghai
}
}不常用但需要知道
声明泛型 record:组件也可以使用类型参数
泛型 record 遵守普通泛型不变性;它适合作为通用返回值,但类型参数的约束仍需要写在声明或方法边界上。
record Pair<L, R>(L left, R right) {}
public class GenericRecordDemo {
public static void main(String[] args) {
Pair<String, Integer> pair = new Pair<>("age", 20);
// 关键变化:pair 接收表达式 new Pair<>("age", 20) 的计算结果。
// 初始状态:pair 的初始值为 new Pair<>("age", 20)。
System.out.println(pair.left() + "=" + pair.right());
// 输出:age=20
}
}在 record 中声明静态成员和业务方法
record 不是只能放字段的哑数据结构,可以声明静态工厂和派生方法;但状态仍应由组件表达,避免把 record 当作可变实体类使用。
record Celsius(double value) {
static Celsius ofFahrenheit(double fahrenheit) {
return new Celsius((fahrenheit - 32) * 5 / 9);
// 返回:按传入华氏温度计算并创建 Celsius;本例 ofFahrenheit(212) 得到 value=100.0。
}
double rounded() {
return Math.round(value * 10) / 10.0;
}
}
public class RecordMethodDemo {
public static void main(String[] args) {
Celsius celsius = Celsius.ofFahrenheit(212);
// 返回:Celsius.ofFahrenheit(212) 计算 value=100.0 并创建记录。
double rounded = celsius.rounded();
// 返回:celsius.rounded() 按一位小数处理 value,rounded=100.0。
System.out.println(rounded);
// 输出:100.0
}
}自定义 equals/hashCode:改变值语义前要谨慎
可以覆盖自动生成的方法,但必须同时保持 equals 与 hashCode 契约;除非领域确实需要,否则优先使用默认的组件值比较。
record CaseInsensitiveName(String value) {
@Override
public boolean equals(Object other) {
return other instanceof CaseInsensitiveName that
&& value.equalsIgnoreCase(that.value);
}
@Override
public int hashCode() {
// 作用:hashCode 将 value.toLowerCase(java.util.Locale.ROOT) 的规范化文本转换为哈希码,保证 equals 相等的对象拥有相同哈希。
return value.toLowerCase(java.util.Locale.ROOT).hashCode();
// 返回:把 value 规范化为小写后计算哈希;"JAVA" 与 "java" 得到相同 hashCode。
}
}
public class RecordEqualityDemo {
public static void main(String[] args) {
CaseInsensitiveName upper = new CaseInsensitiveName("JAVA");
// 初始状态:upper.value() 为 "JAVA"。
CaseInsensitiveName lower = new CaseInsensitiveName("java");
// 初始状态:lower.value() 为 "java"。
boolean same = upper.equals(lower);
// 返回:upper.equals(lower) 忽略大小写比较,same 为 true。
System.out.println(same);
// 输出:true
}
}了解 record 的序列化边界
record 可以声明实现 Serializable,但序列化兼容策略、组件版本演进和敏感字段保护仍由应用负责;不要因为声明了 record 就认为它天然适合长期持久化。
import java.io.Serializable;
record UserSnapshot(String id) implements Serializable {}
public class RecordSerializableDemo {
public static void main(String[] args) {
UserSnapshot snapshot = new UserSnapshot("u-1");
// 初始状态:snapshot.id() 为 "u-1",并实现 Serializable。
System.out.println(snapshot instanceof Serializable);
// 输出:true
}
}简单案例
record Book(String title, int pages) {}
public class RecordSummaryDemo {
public static void main(String[] args) {
Book book = new Book("Java", 300);
// 初始状态:book.title() 为 "Java",book.pages() 为 300。
System.out.println(book.title());
// 输出:Java
}
}record 适合把一组相关字段作为值传递;复杂校验和可变集合边界再回到上面的对应标题。
易混点
- user.name() 是 record 自动生成的访问器,不是 user.getName();框架适配时要显式增加 getter 或映射层。
- final 组件只限制引用重新指向,不能阻止组件对象内部变化;List.copyOf 只提供容器层的浅隔离。
- 紧凑构造器里修改的是参数变量,不能写 this.value = ...;显式规范构造器才需要手动给组件字段赋值。
- record patterns 与模式 switch 在 JDK 20 是预览特性,javac 和 java 两侧都必须带正确开关。
课后小问
record Order(List<String> items) 为什么不能自动保证订单明细不变? 答案:record 的组件引用不可重新赋值,但 List 仍可能是外部传入的可变对象。 解析:在规范构造器中使用 List.copyOf 可以隔离列表结构;如果列表元素自身可变,还需要不可变元素或逐元素复制。
紧凑构造器和显式规范构造器怎么选择? 答案:只需校验或规范化参数时优先紧凑构造器;需要完全控制字段赋值或构造流程时使用显式规范构造器。 解析:两者都必须覆盖全部组件,显式写法要手动完成 this.component = component,紧凑写法由编译器插入赋值。
本节小结
- record 用组件声明数据载体,并自动提供访问器、值比较和字符串表示。
- 紧凑构造器适合校验与规范化,集合组件要显式处理浅不可变边界。
- record 可以实现接口、声明静态工厂和泛型参数,但不能继承普通类。
- JDK 20 的 record patterns 是预览特性,复制案例时要配套编译与运行开关。
快速回顾
- 能写出最小 record 和组件访问器。
- 能解释紧凑构造器与显式规范构造器的区别。
- 能判断 List.copyOf 是否已经实现深不可变。
- 能写出 JDK 20 record pattern 的成对命令。
阿源的知识库