Records 记录类(JDK 14 预览 / JDK 16 正式)
周一早上的架构评审会上,大翔在投影幕上打开了一个 EmployeeDTO.java——197 行代码,只做了一件事:承载数据。
"构造器 15 行,getter 40 行,equals 25 行,hashCode 20 行,toString 15 行……剩下的全是 IDE 生成的样板代码。"大翔滚动着屏幕,"谁写的?"
小崔默默举手:"是我。但这不是标准写法吗?全公司所有 DTO 都这样。"
白歌走到白板前,写下了一行代码:
record Employee(String name, int age) {}。"JDK 16 正式发布的 Records。就这一行,自动生成构造器、getter、equals、hashCode、toString——197 行变 1 行。Lombok 可以卸载了。"
定义表
| 概念 | 描述 |
|---|---|
| Record(记录类) | JDK 16 正式特性,一种特殊的不可变数据载体类,用 record 关键字声明,自动生成全参构造器、访问器方法、equals、hashCode、toString |
| 组件(Component) | Record 头部括号中声明的字段,如 record Point(int x, int y) 中的 x 和 y,每个组件自动生成对应的 public 访问器方法 |
| 紧凑构造器(Compact Constructor) | 不含参数列表的构造器,用于参数校验——编译器自动将校验后的值赋给组件字段 |
| 访问器方法(Accessor Method) | Record 自动生成的方法,名称与组件名相同(如 x() 而非 getX()) |
| 不可变性 | Record 的所有组件字段隐式为 private final,没有 setter,实例创建后不可修改 |
Record vs 传统 POJO / DTO
基础用法:一行定义数据载体
public class RecordBasic {
// === JDK 16:一行定义不可变 Point ===
record Point(int x, int y) {}
public static void main(String[] args) {
Point p1 = new Point(10, 20);
Point p2 = new Point(10, 20);
Point p3 = new Point(30, 40);
// 自动生成的 toString()
System.out.println("p1 = " + p1);
// 自动生成的访问器方法(注意:是 x() 不是 getX())
System.out.println("p1.x = " + p1.x() + ", p1.y = " + p1.y());
// 自动生成的 equals() —— 基于组件值比较
System.out.println("p1.equals(p2) = " + p1.equals(p2)); // true
System.out.println("p1.equals(p3) = " + p1.equals(p3)); // false
// 自动生成的 hashCode()
System.out.println("p1.hashCode() = " + p1.hashCode());
System.out.println("p2.hashCode() = " + p2.hashCode());
}
}
输出:
p1 = Point[x=10, y=20]
p1.x = 10, p1.y = 20
p1.equals(p2) = true
p1.equals(p3) = false
p1.hashCode() = 330
p2.hashCode() = 330
紧凑构造器:优雅的参数校验
传统的构造器参数校验需要写完整的构造方法。Record 的紧凑构造器省略了参数列表和字段赋值语句,编译器自动完成赋值。
public class RecordCompactConstructor {
// === 紧凑构造器:省略参数列表和 this.x = x ===
record Employee(String name, int age) {
// 紧凑构造器 — 编译器自动将校验后的参数赋给字段
Employee {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("姓名不能为空");
}
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年龄必须在 0-150 之间");
}
}
// 可以添加其他实例方法
public String nameWithAge() {
return name + "(" + age + "岁)";
}
}
public static void main(String[] args) {
// === 正常创建 ===
Employee e1 = new Employee("大翔", 35);
System.out.println(e1.nameWithAge());
// === 校验生效 ===
try {
new Employee("", 25);
} catch (IllegalArgumentException e) {
System.out.println("空姓名校验通过: " + e.getMessage());
}
try {
new Employee("白歌", -1);
} catch (IllegalArgumentException e) {
System.out.println("非法年龄校验通过: " + e.getMessage());
}
// === 飞翔科技项目实战:多个 record 组合使用 ===
record Department(String name, String leader) {}
record Project(String name, Department dept, Employee leader) {}
Department tech = new Department("技术部", "白歌");
Project p = new Project("智能运维平台", tech, e1);
System.out.println("\n项目信息: " + p);
}
}
输出:
大翔(35岁)
空姓名校验通过: 姓名不能为空
非法年龄校验通过: 年龄必须在 0-150 之间
项目信息: Project[name=智能运维平台, dept=Department[name=技术部, leader=白歌], leader=Employee[name=大翔, age=35]]
Record 实现接口
Record 可以像普通类一样实现接口,但不能继承父类(Record 隐式继承 java.lang.Record)。
public class RecordInterface {
// === 定义可序列化的接口 ===
interface JsonSerializable {
String toJson();
}
// === Record 实现接口 ===
record ApiResponse(int code, String message, String data) implements JsonSerializable {
@Override
public String toJson() {
return """
{
"code": %d,
"message": "%s",
"data": "%s"
}""".formatted(code, message, data);
}
}
// === 另一个 Record 实现同一接口 ===
record ErrorResponse(int code, String error) implements JsonSerializable {
@Override
public String toJson() {
return """
{
"code": %d,
"error": "%s"
}""".formatted(code, error);
}
}
public static void main(String[] args) {
ApiResponse success = new ApiResponse(200, "查询成功",
"[{\"name\":\"大翔\"},{\"name\":\"白歌\"}]");
ErrorResponse failure = new ErrorResponse(404, "员工不存在");
System.out.println("成功响应 JSON:\n" + success.toJson());
System.out.println("\n错误响应 JSON:\n" + failure.toJson());
// === 多态:用接口类型统一处理 ===
JsonSerializable[] responses = { success, failure };
for (var resp : responses) {
System.out.println("\n--- 统一处理 ---");
System.out.println(resp.toJson());
}
}
}
输出:
成功响应 JSON:
{
"code": 200,
"message": "查询成功",
"data": "[{\"name\":\"大翔\"},{\"name\":\"白歌\"}]"
}
错误响应 JSON:
{
"code": 404,
"error": "员工不存在"
}
--- 统一处理 ---
{
"code": 200,
"message": "查询成功",
"data": "[{\"name\":\"大翔\"},{\"name\":\"白歌\"}]"
}
--- 统一处理 ---
{
"code": 404,
"error": "员工不存在"
}
Record 的静态方法与字段
Record 允许添加静态字段和静态方法,类似于普通类。
import java.util.List;
public class RecordStaticMembers {
record Employee(String name, String department, int salary) {
// === 静态字段(允许)===
private static final String COMPANY = "飞翔科技";
private static final int DEFAULT_SALARY = 10000;
// === 静态工厂方法 ===
public static Employee intern(String name, String department) {
return new Employee(name, department, DEFAULT_SALARY);
}
// === 静态工具方法 ===
public static List<Employee> filterByDept(List<Employee> employees, String dept) {
return employees.stream()
.filter(e -> e.department.equals(dept))
.toList();
}
// === 实例方法:展示信息 ===
public String info() {
return "%s | %s | %s | %d元/月".formatted(COMPANY, name, department, salary);
}
}
public static void main(String[] args) {
// === 静态工厂方法创建 ===
Employee intern = Employee.intern("实习生小明", "技术部");
System.out.println("实习生: " + intern.info());
// === 正常构造 ===
List<Employee> all = List.of(
new Employee("大翔", "技术部", 35000),
new Employee("白歌", "技术部", 30000),
new Employee("Frank", "市场部", 28000),
new Employee("小崔", "技术部", 12000)
);
System.out.println("\n技术部员工:");
Employee.filterByDept(all, "技术部")
.forEach(e -> System.out.println(" " + e.info()));
}
}
输出:
实习生: 飞翔科技 | 实习生小明 | 技术部 | 10000元/月
技术部员工:
飞翔科技 | 大翔 | 技术部 | 35000元/月
飞翔科技 | 白歌 | 技术部 | 30000元/月
飞翔科技 | 小崔 | 技术部 | 12000元/月
易错场景
Record 不能继承类,也不能被继承
public class RecordInheritancePitfall {
// Record 隐式继承 java.lang.Record,且 Record 是 final 的
record Point(int x, int y) {}
public static void main(String[] args) {
// ❌ 编译错误:Record 不能继承其他类
// record Point3D(int x, int y, int z) extends Point {} // 错误
// ❌ 编译错误:其他类不能继承 Record
// class ColoredPoint extends Point {} // 错误
// ✅ Record 可以像 final 类一样通过组合复用
record Point3D(Point point, int z) {
public int x() { return point.x(); }
public int y() { return point.y(); }
}
Point3D p3d = new Point3D(new Point(10, 20), 30);
System.out.println("3D 点: " + p3d);
System.out.println("x=" + p3d.x() + ", y=" + p3d.y() + ", z=" + p3d.z());
}
}
输出:
3D 点: Point3D[point=Point[x=10, y=20], z=30]
x=10, y=20, z=30
不能声明非静态字段
public class RecordNonStaticFieldPitfall {
record Employee(String name, int age) {
// ❌ 编译错误:Record 不能有实例字段(除组件外的非静态字段)
// private String nickname; // 错误
// ✅ 可以定义静态字段
private static final String COMPANY = "飞翔科技";
// ✅ 可以定义实例方法
public String greeting() {
return "%s员工 %s,%d岁".formatted(COMPANY, name, age);
}
}
public static void main(String[] args) {
Employee e = new Employee("大翔", 35);
System.out.println(e.greeting());
}
}
输出:
飞翔科技员工 大翔,35岁
不能修改 Record 组件值
public class RecordImmutabilityPitfall {
record Mutable(int value) {}
public static void main(String[] args) {
Mutable m = new Mutable(42);
System.out.println("初始值: " + m.value());
// ❌ Record 没有 setter,也不能修改字段
// m.value = 100; // 编译错误:组件字段是 final 的
// ✅ 创建新实例来"修改"
// 但 Record 没有自动的 with 方法,需要手动构造
Mutable m2 = new Mutable(100);
System.out.println("新实例值: " + m2.value());
}
}
输出:
初始值: 42
新实例值: 100
面试考点
面试官常问的三个问题:
问题一:"Record 和 Lombok @Data 有什么区别?选哪个?"
答案:核心区别在于语言级支持 vs 第三方注解处理器。Record 是 Java 语言原生特性,编译器直接识别
record关键字并生成字节码,不依赖任何外部工具。Record 强制不可变性(所有字段 final,无 setter),语义更清晰——"这个类就是一个数据载体"。Lombok @Data 则生成可变类(含 setter),依赖注解处理器和 IDE 插件。在新项目中建议优先使用 Record 替代不可变 DTO;如果需要可变性,仍可使用普通类或 Lombok。
问题二:"Record 的 equals() 和 hashCode() 是怎么实现的?"
答案:编译器自动生成基于所有组件的
equals()和hashCode()。equals()使用instanceof检查类型,然后逐一比较每个组件(基本类型用==,引用类型用Objects.equals())。hashCode()基于所有组件的哈希值组合计算。这与 IDE 自动生成的实现逻辑基本一致,但 Record 保证了一致性——你不需要担心生成代码被手动修改导致 equals/hashCode 不一致。
问题三:"紧凑构造器和普通构造器有什么区别?什么时候用?"
答案:紧凑构造器省略了参数列表和字段赋值,专注于参数校验和规范化。语法上只需写
RecordName { ... },编译器自动将参数赋给组件字段。关键规则:在紧凑构造器中对参数名的赋值会在构造器退出时自动写回组件字段。适用于需要校验参数合法性的场景——如非空检查、范围检查、防御性拷贝(引用类型组件)。如果不需要校验,完全不需要写任何构造器,编译器生成的全参构造器即可。
大翔在周报中写道:"本周技术部完成了 DTO 层的 Records 迁移——项目中的 42 个 DTO 类从平均 85 行缩减到平均 12 行,删除了 3066 行样板代码。白歌说得对——Records 不是 Lombok 的替代品,它代表的是 Java 语言对'数据即数据'这一理念的正式认可。"