QUICKREF · LANGUAGE · JEP 395
record:透明数据载体
JDK 17 · 速查主题 01 / 06 · 预计 18 分钟
一句话record 用一行声明描述一组值:字段、访问器、equals/hashCode/toString 都由编译器生成。它的价值是「透明」——你能从声明直接读出对象状态,也因此不适合承载隐藏状态或复杂生命周期。
学完你要能自己回答
- record 自动生成了哪些成员?紧凑构造器与规范构造器分别做什么?
- record 的不可变边界在哪里?List 或数组组件要怎么处理?
- record 能实现接口、能继承类吗?它和 Lombok 生成的类本质区别是什么?
声明即契约01
record 的组件列表同时定义了字段、构造器参数、访问器和 equals/hashCode/toString 的输入。声明是 final class,隐式继承 java.lang.Record,组件字段是 private final。
public record Point(int x, int y) {
public Point {
if (x < 0 || y < 0) {
throw new IllegalArgumentException("坐标必须非负");
}
}
public Point translate(int dx, int dy) {
return new Point(x + dx, y + dy);
}
}
var p = new Point(1, 2);
System.out.println(p);
System.out.println(p.translate(1, 1));
System.out.println(p.x() + "," + p.y());
Point[x=1, y=2] Point[x=2, y=3] 1,2
- 自动生成:规范构造器、每个组件的同名访问器、
equals、hashCode、toString。 - 访问器是
x(),不是getX();toString的格式是Point[x=1, y=2]。 - 可以声明额外方法、静态字段和静态方法,但不能声明额外的实例字段。
- record 不能继承其他类,但可以实现接口;它本身是 final。
record 适合 DTO、返回值、Map 的复合 key、模式匹配的载体。需要 setter、代理、懒加载或复杂生命周期的对象,不要硬套 record。
紧凑构造器:校验与归一化02
规范构造器的签名必须与组件列表一致。紧凑构造器省略参数列表,参数名与组件同名;编译器会在构造器末尾自动插入字段赋值,你只需要写校验或归一化。
public record Range(int from, int to) {
public Range {
if (from > to) {
throw new IllegalArgumentException("from > to");
}
}
public int length() {
return to - from;
}
}
var r = new Range(3, 8);
System.out.println(r);
System.out.println(r.length());
try {
new Range(5, 1);
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
Range[from=3, to=8] 5 from > to
在紧凑构造器里写 from = ... 是在给隐式参数赋值;不能写 this.from = from,字段在构造器体里仍是 final 的未赋值状态,编译器会报 cannot assign a value to final variable。
紧凑构造器适合做参数校验、范围归一化和防御性拷贝。不要在里面做 I/O、查库或昂贵计算:构造器应该快速失败,而不是产生副作用。
不可变是浅的03
record 只保证组件引用不可变,不保证引用指向的对象不可变。List 组件仍可能被外部修改;数组组件的问题更隐蔽,连 equals 都按引用比较。
public record Team(String name, List<String> members) {
public Team {
members = List.copyOf(members);
}
}
var source = new ArrayList<>(List.of("a"));
var team = new Team("core", source);
source.add("b");
System.out.println(team.members().size());
try {
new Team("x", null);
} catch (NullPointerException e) {
System.out.println("null rejected");
}
1 null rejected
List.copyOf返回不可变副本,并拒绝 null;需要保留 null 时不能用它。- 数组组件要在紧凑构造器和访问器里
clone();record 生成的equals对数组按引用比较,内容相同也可能返回 false。 - 需要深不可变时,组件本身也必须是不可变类型;
List<Mutable>仍然是浅不可变。 - record 的
hashCode同样基于组件;数组组件会使用身份哈希,不适合作为 Map key。
record 不是「自动线程安全」的同义词。它只消除了字段重新赋值,不消除共享可变对象带来的竞态。
能力边界04
| 可以做 | 不可以做 |
|---|---|
| 实现接口、声明额外方法 | 声明额外的实例字段 |
| 声明静态字段、静态方法 | 继承其他类(已隐式继承 Record) |
| 定义泛型 record、局部 record、嵌套 record | 定义可变的 setter 语义 |
| 重写访问器、equals、hashCode、toString | 让 record 成为非 final 类 |
| 作为 sealed 层级的 final 子类型 | 依赖无参构造器的框架(如部分 JPA 实体) |
- 局部 record 是隐式的 static,适合方法内临时聚合返回值。
- record 可以实现 sealed 接口,因为它天然是 final,满足 sealed 对子类型的要求。
- 显式重写访问器可以返回派生值或防御性副本,但每次调用都会执行,别放昂贵计算。
- record 的序列化使用规范构造器,反序列化会走校验逻辑;这与普通类的默认反序列化不同。
判断是否用 record,只看一条:对象的状态是否完全由组件列表公开决定。如果还有缓存、连接、监听器或生命周期状态,普通类更合适。
运行时形态05
record 不是语法糖生成的普通类:JVM 与反射 API 都能识别它。Class.isRecord()、getRecordComponents() 是标准接口,框架可以据此做序列化、绑定和校验。
System.out.println(Point.class.isRecord());
System.out.println(java.lang.reflect.Modifier.isFinal(Point.class.getModifiers()));
System.out.println(Point.class.getRecordComponents().length);
true true 2
$ javap -p Point.class
Compiled from "Point.java"
public final class Point extends java.lang.Record {
private final int x;
private final int y;
public Point(int, int);
public Point translate(int, int);
public final java.lang.String toString();
public final int hashCode();
public final boolean equals(java.lang.Object);
public int x();
public int y();
}
equals / hashCode / toString 的方法体不是手写逻辑,而是通过 invokedynamic 链接到 java.lang.runtime.ObjectMethods 的启动方法,由它按组件生成行为。这也是 record 的语义能被运行时统一识别的原因。
17 没有 record pattern。
o instanceof Point(int x, int y) 在 17 连预览都不支持;解构要等到 21(JEP 440)。版本边界BOUNDARY
这张表比 API 本身更值得记。写代码时先确认你的运行时版本。
| 特性 / API | 状态 | 说明 |
|---|---|---|
| record | 16 正式 | JEP 395;JDK 14 / 15 预览。 |
| record pattern | 21 正式 | 17 不支持,连预览都没有;19 / 20 预览。 |
| 局部 record | 16 正式 | 方法内定义,隐式 static。 |
| record 实现 sealed 接口 | 17 可用 | record 天然 final,可作为 sealed 的最终子类型。 |
| record 数组组件的 equals | 语义固定 | 按引用比较,内容相同也可能为 false;需要自定义。 |
费曼自检SELF CHECK
先合上页面,用自己的话回答,再展开对照。能讲出因果和反例,才算真正掌握。
01. record 自动生成了什么?为什么不能声明额外实例字段?
生成规范构造器、同名访问器、equals、hashCode、toString。record 的状态必须完全由组件列表定义,额外实例字段会破坏这种透明性,所以语言直接禁止。
02. 紧凑构造器和规范构造器有什么区别?
规范构造器写出完整参数列表;紧凑构造器省略参数列表,参数名与组件同名,编译器在末尾插入字段赋值。紧凑构造器适合校验和归一化,不能写 this.x = x。
03. record 的 List 组件为什么还需要防御性拷贝?
record 只保证引用不被重新赋值,List 内容仍可被外部修改。应在紧凑构造器里 List.copyOf,访问器里也可返回副本;数组还要 clone,并注意 equals 按引用比较。
04. record 和 Lombok 的 @Value 有什么本质区别?
record 是语言规范的一部分,编译为 final class 并继承 java.lang.Record,语义固定、可被反射识别、可作为模式匹配载体;Lombok 是编译期生成代码,产物仍是普通类,行为可配置。
学习进度
记录保存在本机浏览器 · 不上传