CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Java 进阶/课时/Record 与不可变
RawBlame
课时目录12 节

Java 进阶

  1. 01Java 进阶概览
  2. 02枚举与模式
  3. 03Set/Queue/Stack
  4. 04OOP 进阶
  5. 05方法链与绑定
  6. 06Record 与不可变
  7. 07模块系统
  8. 08并发进阶
  9. 09依赖注入概念
  10. 10Java Web 框架
  11. 11Quarkus 入门
  12. 12综合练习

Record 与不可变

Java 16+ record 类型。

25 分钟

Record 与不可变

这一课你将能做到

  1. 用一行声明定义带校验的 record DTO,并解释编译器自动生成了哪些成员
  2. 处理 record 的不可变边界:紧凑构造器校验、可变组件防御性拷贝、with 风格更新
  3. 判断什么场景该用 record、什么场景必须退回 class(JPA Entity、可变状态)

为什么要学

传统 DTO 一眼望去 80 行:字段、getter/setter、构造器、equals、hashCode、toString——全是机器能生成的噪音。record 把它压成一行,还顺手给了不可变保证:字段天然 final,作 Map key、跨线程传递都不会被半路篡改。Spring Boot 新代码里 record 已经是 Controller 入参出参、MyBatis 查询投影的默认选择。

面试角度:「record 的本质是什么」「record 能被继承吗」「record 和 Lombok @Value 的区别」「为什么 JPA Entity 不用 record」是 JDK 17+ 迁移必考题,本课按能背的标准给出答案。

先建立图像

一行声明:

public record LessonView(String slug, String title, int orderIndex) {}

编译器眼中它等价于:

public final class LessonView extends java.lang.Record {
    private final String slug;
    private final String title;
    private final int orderIndex;

    public LessonView(String slug, String title, int orderIndex) { /* 全参构造 */ }

    public String slug() { return slug; }        // 访问器不带 get 前缀
    public String title() { return title; }
    public int orderIndex() { return orderIndex; }

    @Override public boolean equals(Object o) { /* 全组件参与判等 */ }
    @Override public int hashCode() { /* 全组件参与 */ }
    @Override public String toString() { /* LessonView[slug=..., title=..., orderIndex=...] */ }
}

注意三点:隐式 final(不能被继承)、访问器名与组件同名、没有 setter——不可变是一等公民。

跟着做

步骤 1:完整示例——紧凑构造器、静态工厂、with 风格

新建 RecordDemo.java,完整可运行:

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class RecordDemo {

    /** DTO:一行声明 + 紧凑构造器校验 */
    record LessonView(String slug, String title, int orderIndex) {
        LessonView {   // 紧凑构造器:在字段赋值前执行,直接改写/校验参数
            if (slug == null || slug.isBlank())
                throw new IllegalArgumentException("slug 不能为空");
            if (orderIndex < 0)
                throw new IllegalArgumentException("orderIndex 不能为负");
            title = (title == null || title.isBlank()) ? slug : title;
        }

        /** 静态工厂:语义化创建入口 */
        static LessonView draft(String slug) {
            return new LessonView(slug, slug, 0);
        }

        /** 不可变对象的「修改」= 返回新实例 */
        LessonView withOrder(int newIndex) {
            return new LessonView(slug, title, newIndex);
        }
    }

    public static void main(String[] args) {
        LessonView v = new LessonView("jva-records", "Record 与不可变", 6);
        System.out.println(v.slug());      // jva-records
        System.out.println(v.withOrder(7)); // LessonView[slug=jva-records, title=Record 与不可变, orderIndex=7]

        // 全组件参与判等 ⇒ 值语义
        System.out.println(v.equals(new LessonView("jva-records", "Record 与不可变", 6))); // true

        // 值相等 ⇒ 可安全作 Map key
        Map<LessonView, String> cache = new HashMap<>();
        cache.put(new LessonView("jva-oop", "OOP 进阶", 4), "cached");
        System.out.println(cache.get(new LessonView("jva-oop", "OOP 进阶", 4))); // cached

        System.out.println(LessonView.draft("jva-new"));
    }
}

运行:java RecordDemo.java。

步骤 2:可变组件的防御性拷贝

record 组件如果是 List 这类可变类型,字段引用仍可被外部改动——必须在紧凑构造器里拷贝成不可变结构。把以下内容加入 RecordDemo(局部 record,JDK 16+ 支持):

    public static void main(String[] args) {
        record Tagged(String slug, List<String> tags) {
            Tagged {
                tags = List.copyOf(tags); // 拷贝为不可变列表,外部改不动
            }
        }
        List<String> mutable = new ArrayList<>(List.of("java"));
        Tagged t = new Tagged("x", mutable);
        mutable.add("hack");
        System.out.println(t.tags()); // [java],未被污染
    }

(与 main 合并时注意:局部 record 声明放在原 main 语句之前即可。)

步骤 3(选做):JUnit 验证校验逻辑

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class LessonViewTest {

    @Test
    void blankSlugShouldBeRejected() {
        assertThrows(IllegalArgumentException.class,
                () -> new LessonView(" ", "t", 1));
    }

    @Test
    void negativeOrderShouldBeRejected() {
        assertThrows(IllegalArgumentException.class,
                () -> new LessonView("s", "t", -1));
    }

    @Test
    void withOrderShouldReturnNewInstance() {
        LessonView v = LessonView.draft("jva-records");
        assertNotSame(v, v.withOrder(9));
    }
}

讲清楚要点

  1. record 的本质是「命名的元组」:隐式 final 类,继承 java.lang.Record,组件即 private final 字段,自动生成全参构造器、同名访问器、全组件参与的 equals/hashCode/toString。
  2. 紧凑构造器不是普通构造器:不写参数列表与赋值语句,编译器自动完成字段赋值;你在其中做校验、参数改写(如 title = ... 直接给参数重新赋值)。
  3. 浅不可变:record 只保证组件引用不变,不保证引用对象不可变;List/数组组件必须 List.copyOf 防御性拷贝,否则不可变性是假的。
  4. 值语义带来工程红利:全组件参与 equals/hashCode ⇒ 可安全作 Map key、进 Set、跨线程传递,无需担心哈希值漂移。
  5. record 与 JavaBean 不兼容:没有 getXxx() 与无参构造器,老反射框架(部分老版本 Jackson、JSP EL)可能不适配;Jackson 2.12+ 与 Spring Boot 2.6+ 均已原生支持。
  6. 三类场景退回 class:需要可变状态(Entity 的懒加载代理)、需要继承层次、行为远多于数据。DTO、命令对象、领域事件、查询投影优先 record。

你可能会懵的地方

  • record 能被继承吗?能实现接口吗? 不能被继承(隐式 final),且已继承 java.lang.Record,无法再 extends 别的类;可以实现接口、也可以实现接口的 default 方法。
  • record 和 Lombok @Value 有什么区别? record 是语言级特性:访问器不带 get 前缀、语义上有独立的类型标识、IDE/工具链无插件依赖;@Value 是注解处理器在编译期生成代码,语义上仍是普通类。新代码优先 record,Lombok 用于 Entity/Builder 等场景互补。
  • 为什么 JPA Entity 不用 record? JPA 需要无参构造器(反射实例化)、可变字段(懒加载代理、脏检查更新),record 全部不满足。Entity 用 class,查询投影/DTO 用 record,是标准搭配。
  • record 能有静态字段和静态方法吗? 可以。实例字段只允许组件(防止偷偷加可变状态),但静态字段、静态工厂、实例方法都随意。
  • 组件名能是 status 这种「敏感词」吗? 组件名不能与 Object 方法冲突(如叫 hashcode、tostring 会编译错);status、class 这类要注意,class 是关键字不可用。

错法对照

错法问题正解
record 组件是 List 却不拷贝外部改 List,不可变性被击穿紧凑构造器里 List.copyOf
试图给 record 加 setter破坏不可变契约「修改」用 with 风格返回新实例
给 record 声明实例字段编译报错需要更多状态就加组件,或改用 class
把 record 当 JPA Entity无无参构造、final 字段,代理失效Entity 用 class,record 做 DTO/投影
访问器写成 getSlug()方法不存在,编译错record 访问器是 slug()

卡点急救

  1. 紧凑构造器里赋值没生效:必须给「参数变量」重新赋值(如 title = ...),编译器会用最终参数值赋给字段;给 this.title 赋值反而编译不过。
  2. Jackson 序列化 record 报错:升级 Jackson/Spring Boot(2.6+ 原生支持),或检查是否还挂着老版本 jackson-databind。
  3. 两个「看起来相同」的 record equals 为 false:组件类型不同(int vs Integer)或某个组件没参与判等(自定义了 equals);record 的 equals 由编译器生成,默认全组件参与。

面试常问

  • record 的本质是什么?自动生成了什么? 可背版:隐式 final 的不可变数据载体,继承 java.lang.Record;编译器生成 private final 组件字段、全参构造器、与组件同名的访问器(无 get 前缀)、全组件参与的 equals/hashCode/toString;支持紧凑构造器做参数校验与改写。
  • record 和普通不可变 class 的取舍? record 一行声明、值语义、无样板,适合纯数据;需要继承、可变状态、复杂封装(如内部缓存字段)时用 class + final 字段手写。核心判断:这个类型是「数据」还是「行为主体」。
  • 为什么 record 适合做 Map 的 key / 缓存键? 组件全 final,hashCode 在生命周期内稳定,equals 按全组件值判等;普通可变类作 key 时,字段一改哈希漂移,条目就「找不回来」了——这正是面试常考的「为什么 String 适合做 key」的同款逻辑。

自测三问

  1. record 自动生成的六个成员分别是什么?访问器命名有什么特点?
  2. 紧凑构造器与普通构造器的区别?参数改写怎么写?
  3. 为什么说「record 组件是 List 时不可变性是假的」?如何修复?

小结一句话

Record 用一行声明换来不可变、值语义、零样板;纯数据用 record,有行为有状态留给 class。

延伸

  • Oracle 官方:Records
  • 下一课:模块系统
加载本节练习…
← 上一课方法链与绑定下一课 →模块系统