👋 你好,欢迎来到我的博客!我是【菜鸟不学编程】
   我是一个正在奋斗中的职场码农,步入职场多年,正在从“小码农”慢慢成长为有深度、有思考的技术人。在这条不断进阶的路上,我决定记录下自己的学习与成长过程,也希望通过博客结识更多志同道合的朋友。
  
  🛠️ 主要方向包括 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 constructornew 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
🧵 本文原创,转载请注明出处。

更多推荐