你还在为写 DTO 手抽筋吗?都 Java 14+ 了,Records 不香吗?
👋 你好,欢迎来到我的博客!我是【菜鸟不学编程】
我是一个正在奋斗中的职场码农,步入职场多年,正在从“小码农”慢慢成长为有深度、有思考的技术人。在这条不断进阶的路上,我决定记录下自己的学习与成长过程,也希望通过博客结识更多志同道合的朋友。
🛠️ 主要方向包括 Java 基础、Spring 全家桶、数据库优化、项目实战等,也会分享一些踩坑经历与面试复盘,希望能为还在迷茫中的你提供一些参考。
💡 我相信:写作是一种思考的过程,分享是一种进步的方式。
如果你和我一样热爱技术、热爱成长,欢迎关注我,一起交流进步!
全文目录:
前言:我不是懒,我只是讨厌重复劳动 🙃
说真的,写 Java 写到最后,很多人的日常是这样的:
- 创建一个
UserDTO - 写一堆字段
- 生成 getter/setter
equals/hashCode/toString一键生成- 然后第二天字段改了,你再来一遍
- 最后你对着 IDE 发呆:“我到底是在做业务,还是在给样板戏抄台词?”
我以前还硬撑着说“这就是工程规范”。后来我遇到了 Records,当场就一个反应:
“哦……原来我不是不爱写代码,我只是不爱写废话。”😌
Records 不是万能钥匙,但它确实能把“数据载体类”从体力活变成脑力活:
把注意力从“怎么写一堆方法”拉回到“这个对象表达了什么”。
I. Records 引入:Java 14+ 的不可变数据载体类
Records 是 Java 为“纯数据对象”提供的语言级能力(Java 14 预览,Java 16 正式)。它解决的是:
“我只是想表达一坨数据,为什么要写 80 行?”
Records 的核心气质就俩字:不可变(更准确说:组件引用不可变)。
你可以把它理解成:
- 更轻量的 DTO / VO
- 更可信的值对象(Value Object)
- 更少 bug 的“数据快递盒”📦
友情提醒:Records 是“浅不可变”——字段引用不能变,但如果字段本身是可变对象(比如
List),那你还是可能翻车,后面会讲怎么防。
II. 定义 Records:组件、自动生成的 equals、hashCode 和 toString
1)最小可用的 Record
public record User(String id, String name, int age) {}
就这?就这。
编译器会自动帮你生成:
private final字段(叫 组件 components)- canonical constructor:
new User(id, name, age) - 访问器方法(注意不是
getName(),而是name()) equals()、hashCode()、toString()
你打印一下:
var u = new User("u_1", "Primavera", 20);
System.out.println(u);
// User[id=u_1, name=Primavera, age=20]
这 toString 的可读性,老实说,比不少人手写的都干净。😄
2)组件 ≠ 字段命名的小细节
Record 的字段名就是 API 的一部分。
如果你以前 DTO 里字段随手写 a b c,那你用 Record 会很难受——因为你会被迫“像个正常人一样命名”。
(是的,这算一种温柔的强迫症治疗 🥲)
III. 自定义行为:添加方法、静态字段和嵌套 Records
很多人以为 record 就是“只能装数据”。错,record 可以很“有灵魂”。
1)添加业务方法:record 也能有行为
比如我们希望 User 能判断是否成年:
public record User(String id, String name, int age) {
public boolean isAdult() {
return age >= 18;
}
}
2)静态字段 / 静态方法:放“共识”,别放“状态”
你可以加常量、工厂方法:
public record User(String id, String name, int age) {
public static final int ADULT_AGE = 18;
public static User of(String id, String name, int age) {
return new User(id, name, age);
}
}
但别往 record 里塞可变静态状态(比如缓存 Map),那就不是“数据载体”,而是“全村的希望”,迟早出事。
3)校验:用 compact constructor 把脏数据挡门外 🚪
这是 record 真正“工程上很香”的地方:
你可以在构造时做校验,保证对象一旦创建就是合法的。
import java.util.Objects;
public record User(String id, String name, int age) {
public User {
Objects.requireNonNull(id, "id 不能为空");
Objects.requireNonNull(name, "name 不能为空");
if (name.isBlank()) throw new IllegalArgumentException("name 不能为空字符串");
if (age < 0) throw new IllegalArgumentException("age 不能为负数");
}
}
这个 public User { ... } 就是 compact constructor:
- 你不用写参数列表
- 编译器会自动把传入的值赋给组件字段
- 你只负责“守门”✅
说人话:record 鼓励你把“不合法状态”拒之门外,而不是让它渗进系统里,等线上炸给你看。😇
4)嵌套 Records:组织结构更清爽
例如 User 里有地址:
public record User(String id, String name, Address address) {
public record Address(String province, String city, String detail) {
public Address {
if (province == null || province.isBlank()) throw new IllegalArgumentException("province 不能为空");
if (city == null || city.isBlank()) throw new IllegalArgumentException("city 不能为空");
}
}
}
这种写法对“领域模型分层”很友好:
结构清晰、文件少、读起来像个完整句子。
IV. 与 Lombok 比较:Records vs. @Data 注解
好,来,咱们不打太极,直接上结论:
Records 不是 Lombok 的替代品,但它在“数据载体”这块确实更硬核。
1)Records 的优势(偏工程稳定)
- 语言级支持:不靠注解处理器,不怕构建链路/IDE 配置偶发抽风
- 默认不可变:减少“对象被改坏”的概率
- 语义更明确:看到
record,你就知道它是“值”,不是“实体” - equals/hashCode 更符合值对象习惯:按组件比较(更直觉)
2)Lombok @Data 的优势(偏快速与兼容)
- 适合遗留项目:一把梭,把 DTO 改造成本更低
@Data自带 getter/setter:对很多框架“开箱即用”- 可以做可变对象(但可变对象也容易出锅…)
3)最大的差别:默认可变 vs 默认不可变
@Data默认给你 setter- record 默认不给你 setter
这不是小区别,这是“系统性格”的区别:
可变对象像橡皮泥,方便但容易被捏歪;不可变对象像积木,组合麻烦一点,但稳定。
4)一句现实建议(不站队)
- 你要表达 “数据快照 / DTO / 配置 / 值对象”:优先 record
- 你要表达 “实体对象 / 需要逐步填充的对象 / 与某些框架深度绑定的可变 bean”:Lombok 可能更顺手
别纠结“谁更高级”,看场景,别上头。🙂
V. 使用场景:DTO、配置类和不可变对象
1)DTO:接口层传输对象(最常见)
- Controller 入参/出参
- RPC/HTTP 传输结构
- 内部 service 的“数据包”
record 的好处是:
字段一眼看清、默认不可变、toString 友好,排查日志也舒服。
2)配置类:表达“启动时就定死”的配置
比如:
public record AppConfig(String jdbcUrl, int poolSize, boolean debug) {
public AppConfig {
if (jdbcUrl == null || jdbcUrl.isBlank()) throw new IllegalArgumentException("jdbcUrl 不能为空");
if (poolSize <= 0) throw new IllegalArgumentException("poolSize 必须 > 0");
}
}
配置天然适合不可变——你总不想线上跑着跑着配置被谁改了吧?那也太刺激了。😨
3)不可变值对象:钱、坐标、范围、标识符
例如 Money:
import java.math.BigDecimal;
import java.util.Currency;
public record Money(BigDecimal amount, Currency currency) {
public Money {
if (amount == null || amount.signum() < 0) throw new IllegalArgumentException("amount 必须 >= 0");
if (currency == null) throw new IllegalArgumentException("currency 不能为空");
}
public Money add(Money other) {
if (!currency.equals(other.currency)) throw new IllegalArgumentException("币种不同不能相加");
return new Money(amount.add(other.amount), currency);
}
}
record 在 DDD(领域驱动)里当 VO 用,真的挺顺。
VI. 项目:使用 Records 实现一个用户数据模型(可运行示例)
下面我给你一个小而完整的“用户模型”骨架:
- User record(含校验)
- Value Objects:UserId、Email
- Role 枚举
- 用户更新:用“with”风格方法返回新对象(不可变的正确打开方式)
- 一个极简 Repository + Service 演示用法
我写得偏“像真项目”,不是 HelloWorld 那种糊弄人的。
1)领域值对象:UserId、Email
import java.util.Objects;
import java.util.UUID;
import java.util.regex.Pattern;
public record UserId(String value) {
public UserId {
Objects.requireNonNull(value, "UserId 不能为空");
if (value.isBlank()) throw new IllegalArgumentException("UserId 不能为空字符串");
}
public static UserId newId() {
return new UserId("u_" + UUID.randomUUID());
}
}
public record Email(String value) {
private static final Pattern EMAIL =
Pattern.compile("^[\\w.%+-]+@[\\w.-]+\\.[A-Za-z]{2,}$");
public Email {
Objects.requireNonNull(value, "Email 不能为空");
if (!EMAIL.matcher(value).matches()) {
throw new IllegalArgumentException("Email 格式不合法: " + value);
}
}
}
为什么要值对象?
因为你不希望系统里到处都是 String,然后你永远不知道这个 String 是 id、邮箱还是昵称。
(String 的自由,有时候是灾难的开始…😮💨)
2)Role:枚举表达用户权限
public enum Role {
USER,
ADMIN
}
3)User record:核心用户模型(含校验 + with 方法)
import java.time.Instant;
import java.util.Objects;
public record User(
UserId id,
String name,
Email email,
Role role,
Instant createdAt
) {
public User {
Objects.requireNonNull(id, "id 不能为空");
Objects.requireNonNull(email, "email 不能为空");
Objects.requireNonNull(role, "role 不能为空");
Objects.requireNonNull(createdAt, "createdAt 不能为空");
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("name 不能为空");
}
if (name.length() > 40) {
throw new IllegalArgumentException("name 太长了(<=40)");
}
}
/** 工厂方法:创建新用户 */
public static User create(String name, String email) {
return new User(
UserId.newId(),
name,
new Email(email),
Role.USER,
Instant.now()
);
}
/** “更新昵称”不是改字段,而是返回一个新对象 */
public User withName(String newName) {
return new User(id, newName, email, role, createdAt);
}
public User withRole(Role newRole) {
return new User(id, name, email, newRole, createdAt);
}
}
这里的点很关键:
record 不可变,所以“更新”要用 返回新对象 的方式。
你一开始会觉得麻烦,但当你线上排查“是谁把 role 改成 ADMIN 的”时,你会突然理解不可变的温柔。🙂
4)Repository:用 Map 模拟持久化(演示结构)
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
public class UserRepository {
private final Map<String, User> store = new ConcurrentHashMap<>();
public User save(User user) {
store.put(user.id().value(), user);
return user;
}
public Optional<User> findById(UserId id) {
return Optional.ofNullable(store.get(id.value()));
}
public Optional<User> findByEmail(Email email) {
return store.values().stream()
.filter(u -> u.email().value().equalsIgnoreCase(email.value()))
.findFirst();
}
}
5)Service:注册、升级权限、改名(演示业务流)
public class UserService {
private final UserRepository repo;
public UserService(UserRepository repo) {
this.repo = repo;
}
public User register(String name, String email) {
var exists = repo.findByEmail(new Email(email));
if (exists.isPresent()) {
throw new IllegalStateException("邮箱已注册: " + email);
}
var user = User.create(name, email);
return repo.save(user);
}
public User promoteToAdmin(UserId id) {
var user = repo.findById(id)
.orElseThrow(() -> new IllegalArgumentException("用户不存在: " + id.value()));
var updated = user.withRole(Role.ADMIN);
return repo.save(updated);
}
public User rename(UserId id, String newName) {
var user = repo.findById(id)
.orElseThrow(() -> new IllegalArgumentException("用户不存在: " + id.value()));
var updated = user.withName(newName);
return repo.save(updated);
}
}
6)Main:跑一下,看看“不可变模型”怎么工作
public class Main {
public static void main(String[] args) {
var repo = new UserRepository();
var service = new UserService(repo);
var user = service.register("小春", "primavera@example.com");
System.out.println("注册用户: " + user);
var renamed = service.rename(user.id(), "小春春");
System.out.println("改名后: " + renamed);
var admin = service.promoteToAdmin(user.id());
System.out.println("升级为管理员: " + admin);
// 你会发现:每一步都是“新对象”,不会把旧对象悄悄改脏
System.out.println("原始对象还在吗?还在: " + user);
}
}
写到这儿,我想再反问你一句(很认真那种)
你写 DTO 的时候,真的享受那一堆 getter/setter 吗?
还是你只是习惯了“反正一直这么写”?🤔
Records 的价值不在于“少写几行代码”,而在于:
- 让数据对象更可信(创建即合法)
- 让系统更好推理(不可变减少副作用)
- 让表达更贴近业务(UserId、Email 不再是迷之 String)
小结:Records 什么时候用,什么时候别硬上
✅ 推荐用 record:
- DTO / API 入参出参
- 配置对象
- 值对象(Money、Email、Range…)
- 需要可靠
equals/hashCode的数据结构 key
⚠️ 谨慎甚至不建议:
- JPA 实体(很多情况下需要无参构造、延迟加载、代理等;record 天生不太合拍)
- 需要频繁“逐步填充”的可变对象
- 字段里塞大量可变集合但又不做防御性拷贝(容易“浅不可变翻车”)
📝 写在最后
如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!
我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!
感谢你的阅读,我们下篇文章再见~👋
✍️ 作者:某个被流“治愈”过的 Java 老兵
📅 日期:2026-01-07
🧵 本文原创,转载请注明出处。
更多推荐


所有评论(0)