JDK14 Record类:不可变数据对象的终极解决方案
2026/4/30大约 11 分钟
JDK14 Record类:不可变数据对象的终极解决方案
适读人群:写过大量Lombok @Data代码、对Java样板代码感到厌倦的开发者 | 阅读时长:约16分钟
开篇故事
我在2021年之前,项目里的DTO类是这样的(Lombok版):
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class UserDTO {
private Long id;
private String name;
private String email;
private LocalDateTime createdAt;
}一个简单的数据传输对象,需要加四五个注解,还需要引入Lombok依赖,还要担心Lombok版本和JDK版本的兼容性。
更麻烦的是:@Data生成的是可变对象(有setter),在多线程场景下需要额外小心。我们项目里曾经有人把DTO放到了缓存里,然后另一个地方修改了DTO的字段,导致缓存数据错误,排查了半天。
JDK14引入Record类,一行代码解决了所有问题:
record UserDTO(Long id, String name, String email, LocalDateTime createdAt) {}不仅代码量减少90%,还是天然的不可变对象。今天把Record彻底讲透。
一、Record类的设计背景
1.1 Java数据类的历史问题
在Record出现之前,Java创建一个"纯数据容器类"需要:
手写版本(标准Java Bean):
✗ 私有字段声明(N行)
✗ 构造方法(N行)
✗ 每个字段的getter(N行)
✗ equals()(10-30行)
✗ hashCode()(10-20行)
✗ toString()(10-20行)
Lombok版本:
✗ 需要Lombok依赖
✗ 注解组合容易出错(@Data = @Getter + @Setter + @EqualsAndHashCode + @ToString + @RequiredArgsConstructor)
✗ 可变对象带来线程安全隐患
✗ IDE插件+Lombok插件+编译器注解处理,链路复杂1.2 Record的设计哲学
Record是一种"名义元组"(Nominal Tuple),它的核心理念是:
"一个Record类就是声明:这个类的唯一目的就是携带数据,它的行为完全由这些数据决定。"
引入版本:JDK14(Preview,2020年3月)
正式GA:JDK16(2021年3月,JEP 395)
设计文档:JEP 395(https://openjdk.org/jeps/395)二、Record类深度解析
2.1 编译器自动生成什么
// 你写的代码:
record Point(int x, int y) {}
// 编译器等价生成(伪代码展示):
final class Point extends java.lang.Record {
private final int x; // 自动final
private final int y;
// 规范构造方法
public Point(int x, int y) {
this.x = x;
this.y = y;
}
// 访问器方法(注意:不是getX(),是x())
public int x() { return x; }
public int y() { return y; }
// equals:基于所有组件的值比较
@Override
public boolean equals(Object o) {
if (!(o instanceof Point other)) return false;
return x == other.x && y == other.y;
}
// hashCode:基于所有组件
@Override
public int hashCode() {
return Objects.hash(x, y);
}
// toString
@Override
public String toString() {
return "Point[x=" + x + ", y=" + y + "]";
}
}2.2 Record的限制
Record类的限制:
1. 不能继承其他类(已隐式继承java.lang.Record)
2. 隐式final,不能被继承
3. 组件字段隐式private final,不能修改
4. 不能声明实例字段(只能在组件里)
5. 可以实现接口
Record类的能力:
✓ 可以声明static字段和static方法
✓ 可以自定义构造方法(紧凑构造)
✓ 可以重写toString/equals/hashCode
✓ 可以添加实例方法
✓ 可以实现接口
✓ 支持注解
✓ 支持泛型2.3 Record的内部结构
┌─────────────────────────────────────────────────────────┐
│ Record 结构 │
│ │
│ record Person(String name, int age) { │
│ ①组件列表─────────────────────┘ │
│ │
│ ② 自动生成: │
│ - final字段: private final String name; │
│ private final int age; │
│ - 规范构造: public Person(String name, int age) │
│ - 访问器: public String name() │
│ public int age() │
│ - equals/hashCode/toString │
│ │
│ ③ 允许自定义(可选): │
│ - 紧凑构造(校验/规范化) │
│ - 自定义方法 │
│ - 实现接口 │
│ - static成员 │
└─────────────────────────────────────────────────────────┘三、完整代码示例
3.1 基础用法:旧写法到新写法的对比
import java.util.*;
import java.time.*;
import java.util.Objects;
/**
* Record类完整示例
* 引入版本:JDK14 Preview;GA版本:JDK16(2021年3月)
*/
// ===== 旧写法:手写不可变DTO =====
public final class UserDTOOld {
private final Long id;
private final String name;
private final String email;
public UserDTOOld(Long id, String name, String email) {
this.id = Objects.requireNonNull(id);
this.name = Objects.requireNonNull(name);
this.email = Objects.requireNonNull(email);
}
public Long getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof UserDTOOld)) return false;
UserDTOOld that = (UserDTOOld) o;
return Objects.equals(id, that.id) &&
Objects.equals(name, that.name) &&
Objects.equals(email, that.email);
}
@Override
public int hashCode() { return Objects.hash(id, name, email); }
@Override
public String toString() {
return "UserDTOOld{id=" + id + ", name=" + name + ", email=" + email + "}";
}
}
// ===== 新写法:Record =====
// 一行代码,自动生成上面所有内容
record UserDTO(Long id, String name, String email) {
// 紧凑构造方法(Compact Constructor):用于参数校验和规范化
// 注意:不写参数列表,隐式持有所有组件参数
public UserDTO {
Objects.requireNonNull(id, "id cannot be null");
Objects.requireNonNull(name, "name cannot be null");
// 规范化:trim空格
name = name.trim(); // 这里可以修改组件值(在规范构造中)
if (name.isEmpty()) throw new IllegalArgumentException("name cannot be empty");
Objects.requireNonNull(email, "email cannot be null");
email = email.toLowerCase(); // 统一转小写
}
// 自定义方法
public String displayName() {
return name + " <" + email + ">";
}
// static工厂方法
public static UserDTO of(Long id, String name, String email) {
return new UserDTO(id, name, email);
}
// 可以重写自动生成的方法
@Override
public String toString() {
return "User{id=" + id + ", display=" + displayName() + "}";
}
}3.2 高级用法:泛型Record、接口实现、嵌套Record
import java.util.*;
import java.util.function.*;
/**
* Record高级用法示例
*/
public class RecordAdvancedDemo {
// ===== 泛型Record =====
record Pair<A, B>(A first, B second) {
// 静态工厂
public static <A, B> Pair<A, B> of(A first, B second) {
return new Pair<>(first, second);
}
// 映射方法(返回新Record)
public <C> Pair<C, B> mapFirst(Function<A, C> mapper) {
return new Pair<>(mapper.apply(first), second);
}
public Pair<B, A> swap() {
return new Pair<>(second, first);
}
}
// ===== Record实现接口 =====
interface Displayable {
String display();
}
interface Validatable {
boolean isValid();
}
record Email(String address) implements Displayable, Validatable {
public Email {
Objects.requireNonNull(address, "email address cannot be null");
address = address.trim().toLowerCase();
}
@Override
public String display() {
return "<" + address + ">";
}
@Override
public boolean isValid() {
return address.contains("@") && address.contains(".");
}
}
// ===== 嵌套Record(值对象建模)=====
record Address(String street, String city, String country) {
public Address {
Objects.requireNonNull(street);
Objects.requireNonNull(city);
Objects.requireNonNull(country);
}
}
record PhoneNumber(String countryCode, String number) {
public PhoneNumber {
if (!countryCode.startsWith("+")) {
countryCode = "+" + countryCode;
}
}
public String formatted() {
return countryCode + " " + number;
}
}
record ContactInfo(Email email, PhoneNumber phone, Address address) {}
record Customer(long id, String name, ContactInfo contact) {
// Record可以有静态字段
private static final int MAX_NAME_LENGTH = 100;
public Customer {
if (name.length() > MAX_NAME_LENGTH) {
throw new IllegalArgumentException("Name too long");
}
}
public String primaryContact() {
return contact.email().display();
}
}
// ===== "wither"模式:生成修改了某个字段的新实例 =====
record ImmutableConfig(String host, int port, boolean ssl, int timeout) {
// 传统做法需要手写这些"with"方法
public ImmutableConfig withHost(String newHost) {
return new ImmutableConfig(newHost, port, ssl, timeout);
}
public ImmutableConfig withPort(int newPort) {
return new ImmutableConfig(host, newPort, ssl, timeout);
}
public ImmutableConfig withSsl(boolean newSsl) {
return new ImmutableConfig(host, port, newSsl, timeout);
}
public ImmutableConfig withTimeout(int newTimeout) {
return new ImmutableConfig(host, port, ssl, newTimeout);
}
// 静态默认配置
public static ImmutableConfig defaultConfig() {
return new ImmutableConfig("localhost", 8080, false, 30000);
}
}
public static void main(String[] args) {
// 泛型Record
var pair = Pair.of("hello", 42);
System.out.println(pair); // Pair[first=hello, second=42]
System.out.println(pair.swap()); // Pair[first=42, second=hello]
System.out.println(pair.mapFirst(String::length)); // Pair[first=5, second=42]
// Record实现接口
var email = new Email(" Alice@Example.COM ");
System.out.println(email.display()); // <alice@example.com>
System.out.println(email.isValid()); // true
// 嵌套Record
var customer = new Customer(
1L, "Alice",
new ContactInfo(
new Email("alice@example.com"),
new PhoneNumber("86", "13800138000"),
new Address("100 Main St", "Beijing", "China")
)
);
System.out.println(customer.primaryContact()); // <alice@example.com>
// wither模式
var config = ImmutableConfig.defaultConfig();
var prodConfig = config.withHost("prod.server.com")
.withPort(443)
.withSsl(true)
.withTimeout(60000);
System.out.println(prodConfig);
// ImmutableConfig[host=prod.server.com, port=443, ssl=true, timeout=60000]
// Record的equals基于值
var p1 = new Pair<>("a", 1);
var p2 = new Pair<>("a", 1);
System.out.println(p1.equals(p2)); // true(值相等)
System.out.println(p1 == p2); // false(不同实例)
}
}3.3 Record在实际项目中的应用场景
import java.util.*;
import java.util.stream.*;
import java.time.*;
/**
* Record在实际项目中的5个典型场景
*/
public class RecordInProduction {
// ===== 场景1:API响应DTO =====
// 旧写法(Lombok):
// @Data @Builder @NoArgsConstructor @AllArgsConstructor
// class UserResponse { ... }
// 新写法:
record UserResponse(long id, String name, String email, LocalDateTime createdAt) {}
record PageResponse<T>(List<T> items, long total, int page, int pageSize) {
public int totalPages() {
return (int) Math.ceil((double) total / pageSize);
}
public boolean hasNext() {
return page < totalPages();
}
}
// ===== 场景2:查询条件封装 =====
record UserQuery(
String nameKeyword,
String emailDomain,
Integer minAge,
Integer maxAge,
boolean activeOnly
) {
// 使用Builder模式(Record本身没有Builder,手写一个)
public static Builder builder() { return new Builder(); }
public static class Builder {
private String nameKeyword;
private String emailDomain;
private Integer minAge;
private Integer maxAge;
private boolean activeOnly = true;
public Builder nameKeyword(String v) { this.nameKeyword = v; return this; }
public Builder emailDomain(String v) { this.emailDomain = v; return this; }
public Builder minAge(Integer v) { this.minAge = v; return this; }
public Builder maxAge(Integer v) { this.maxAge = v; return this; }
public Builder activeOnly(boolean v) { this.activeOnly = v; return this; }
public UserQuery build() {
return new UserQuery(nameKeyword, emailDomain, minAge, maxAge, activeOnly);
}
}
}
// ===== 场景3:事件/消息对象(不可变性很重要)=====
record UserCreatedEvent(
String eventId,
long userId,
String userName,
Instant occurredAt
) {
public UserCreatedEvent(long userId, String userName) {
this(UUID.randomUUID().toString(), userId, userName, Instant.now());
}
}
record OrderPlacedEvent(String orderId, long userId, double totalAmount, Instant occurredAt) {
public OrderPlacedEvent(String orderId, long userId, double totalAmount) {
this(orderId, userId, totalAmount, Instant.now());
}
}
// ===== 场景4:Map.Entry的替代(方法返回多个值)=====
// 旧写法:return Map.entry(user, orderCount); // 语义不清
// 新写法:
record UserWithOrderCount(UserResponse user, long orderCount) {}
static List<UserWithOrderCount> getUsersWithOrderCount(List<UserResponse> users) {
return users.stream()
.map(user -> new UserWithOrderCount(user, fetchOrderCount(user.id())))
.collect(Collectors.toList());
}
static long fetchOrderCount(long userId) {
return userId * 3; // 模拟查询
}
// ===== 场景5:坐标/范围等值对象 =====
record Range<T extends Comparable<T>>(T min, T max) {
public Range {
Objects.requireNonNull(min);
Objects.requireNonNull(max);
if (min.compareTo(max) > 0) {
throw new IllegalArgumentException("min must be <= max");
}
}
public boolean contains(T value) {
return value.compareTo(min) >= 0 && value.compareTo(max) <= 0;
}
public boolean overlaps(Range<T> other) {
return max.compareTo(other.min) >= 0 && other.max.compareTo(min) >= 0;
}
}
public static void main(String[] args) {
// 场景1:API响应
var users = List.of(
new UserResponse(1L, "Alice", "alice@example.com", LocalDateTime.now()),
new UserResponse(2L, "Bob", "bob@example.com", LocalDateTime.now())
);
var page = new PageResponse<>(users, 100L, 1, 2);
System.out.printf("Page %d of %d, has next: %b%n",
page.page(), page.totalPages(), page.hasNext());
// 场景2:查询条件
var query = UserQuery.builder()
.emailDomain("example.com")
.minAge(18)
.activeOnly(true)
.build();
System.out.println(query);
// 场景3:事件对象(不可变,安全传递)
var event = new UserCreatedEvent(1L, "Alice");
System.out.println(event);
// 场景4:多返回值
var usersWithOrders = getUsersWithOrderCount(users);
usersWithOrders.forEach(u ->
System.out.printf("%s has %d orders%n", u.user().name(), u.orderCount()));
// 场景5:范围对象
var priceRange = new Range<>(10.0, 100.0);
System.out.println(priceRange.contains(50.0)); // true
System.out.println(priceRange.contains(200.0)); // false
System.out.println(priceRange.overlaps(new Range<>(50.0, 150.0))); // true
}
}四、踩坑实录
坑1:Record没有setter,无法与某些框架(Jackson、JPA)直接集成
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
// 问题1:Jackson默认不支持Record的反序列化(JDK8时代的Jackson)
record UserDTO(Long id, String name) {}
// 解决方案1:使用Jackson 2.12+,自动支持Record
// ObjectMapper mapper = new ObjectMapper();
// 在JDK16+环境下,Jackson 2.12+已自动支持
// 解决方案2:如果用旧版Jackson,添加注解
// @JsonDeserialize // 配合构造方法注解
record UserDTOWithAnnotation(
@JsonProperty("id") Long id,
@JsonProperty("name") String name
) {}
// 问题2:JPA/Hibernate不支持Record作为Entity
// Record是不可变的,JPA Entity必须可变(需要无参构造+setter)
// 解决方案:Record只用于DTO层,Entity层还是用普通类
// @Entity
// record UserEntity(Long id, String name) {} // 错误!JPA不支持
// 正确分层使用:
// Entity(可变)-> 业务层 -> Record DTO(不可变)-> API层坑2:Record在Lombok项目中的混用问题
// 问题:Record和Lombok混用时,注解处理器可能有冲突
// 如果已有@Data类,迁移时注意:
// 1. Record的访问器是x()而不是getX(),接口约定可能需要调整
// 2. Lombok @Builder和Record不兼容,需要手写Builder
// 迁移示例:
// 旧:
// @Data
// class OldUser { private String name; private String email; }
// String name = user.getName(); // 旧的getter命名
// user.setName("Alice"); // setter存在
// 新:
record NewUser(String name, String email) {}
// String name = user.name(); // 新的accessor命名(无get前缀)
// 没有setter!需要创建新对象坑3:序列化和反序列化版本兼容
// 问题:Record的serialVersionUID自动计算,可能在JDK版本升级后改变
// Record的序列化机制与普通类不同,不能使用serialPersistentFields等
// 危险:将Record直接用于Serializable并存入持久化存储
record SerializableRecord(String data) implements java.io.Serializable {
// 可以,但有风险:
// 1. 字段顺序改变 -> 反序列化失败
// 2. JDK版本升级 -> serialVersionUID变化
}
// 更安全的做法:Record只用于内存中传输,持久化用JSON/Protobuf等坑4:equals/hashCode只比较声明的组件
// Record的equals/hashCode只包含组件列表中声明的字段
// 如果有额外的计算字段(transient/derived),不会参与比较
record ProductWithCache(String id, String name, double price) {
// 假设我们添加了一个缓存字段(实际上Record不允许实例字段,这里演示概念)
// 如果有transient字段,它不会出现在equals中
// 计算字段也不参与equals
public double discountedPrice() {
return price * 0.9; // 这个不参与equals
}
}
// 所以两个Record:
var p1 = new ProductWithCache("001", "Apple", 3.0);
var p2 = new ProductWithCache("001", "Apple", 3.0);
// p1.equals(p2) == true(组件值相同)
// 即使discountedPrice()相同,也已经体现在price里了坑5:Pattern Matching中使用Record(JDK21特性,提前了解)
// JDK21引入Record Pattern,和Record深度集成
// 这是Record最强大的特性之一(详见第398期)
// JDK16(仅Record基础):
Object obj = new UserDTO(1L, "Alice", "alice@example.com");
if (obj instanceof UserDTO user) {
System.out.println(user.name()); // instanceof类型模式,JDK16
}
// JDK21(Record解构):
if (obj instanceof UserDTO(var id, var name, var email)) {
System.out.println(name); // 直接解构!
}五、总结与延伸
5.1 Record vs Lombok @Data vs 手写类
| 对比项 | 手写类 | Lombok @Data | Record |
|---|---|---|---|
| 代码量 | 多 | 少(注解) | 极少(一行) |
| 不可变性 | 手动保证 | 不保证(有setter) | 天然不可变 |
| 线程安全 | 需要考虑 | 可变,不安全 | 安全(不可变) |
| 依赖 | 无 | Lombok | 无 |
| IDE支持 | 完整 | 需要插件 | 完整 |
| JDK版本 | 任意 | 任意 | JDK16+ |
| 继承 | 可以 | 可以 | 不能 |
| JPA Entity | 可以 | 可以 | 不能 |
5.2 推荐的使用场景
✓ 强烈推荐用Record:
- DTO(数据传输对象)
- 值对象(Value Object,DDD概念)
- 方法返回多个值的容器
- 事件对象(Event)
- 查询结果封装
- 配置对象(不可变的)
✗ 不应该用Record:
- JPA/MyBatis Entity(需要可变+无参构造)
- 需要继承/被继承的类
- 需要懒加载字段的类
- 字段很多且需要Builder的复杂对象5.3 版本兼容建议
- JDK14~15:Record是Preview特性,需要
--enable-preview,不推荐生产使用 - JDK16:Record正式GA,可以生产使用
- JDK17(LTS):Record稳定,Jackson 2.12+、Spring Framework 5.3+均支持
- JDK21(LTS):配合Record Patterns(第398期),Record的能力更强大
