CodeGym /课程 /JAVA 25 SELF /Record:语法与优势

Record:语法与优势

JAVA 25 SELF
第 22 级 , 课程 0
可用

1. “DTO 类”的问题:我们为什么需要 record 类?

老实说:你写过多少次这样的类?


public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Point point = (Point) o;
        return x == point.x && y == point.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point{" + "x=" + x + ", y=" + y + '}';
    }
}

看起来不难,但数数有多少行!现在想象一下,你有 10 个这样的类,每个类还有 5–6 个字段。即便是 IDE 也会“厌倦”生成这些模板代码。而且如果你决定增加一个新字段——你就得修改构造器、getter、equalshashCodetoString……枯燥、重复,还容易出错。

这样的类被称为 DTO(Data Transfer Object,数据传输对象)或 Value Object(值对象)。 它们只是存放数据——就这样。但由于模板代码过多,维护起来很费劲。

如果你觉得这不是问题,那就等等看,等到你需要同时修改 50 个这样的类的时候。到那时你会格外怀念 record 类!

2. 了解 record:语法与 Java 16+ 的“魔法”

从 Java 16 开始,一切都变了。出现了一种新的类——record。它就是为“只需存放一组数据”的场景而生的。语法几乎类似其他语言里的元组。

如何声明 record?


public record Point(int x, int y) { }

就这样!你刚刚创建了一个包含两个字段、带构造器、访问器、equalshashCodetoString 的不可变类。无需多余的样板代码。

Java 在“幕后”做了什么?

  • 字段 xy 会变成 private final
  • 访问器 会自动生成:int x()int y()
  • 构造器public Point(int x, int y)
  • equals/hashCode:按值比较所有字段。
  • toString:返回形如 "Point[x=1, y=2]" 的字符串。

可以说,record 是“加料版 DTO”:更少的代码,更多的保障,更少的 bug。

不可变性(immutability)

record 类的所有字段都会自动成为 final。对象一旦创建就无法修改——编译器会为此提供保证。

如果你尝试添加 setter 或让某个字段不是 final——编译器会阻止你。这种对你“身心健康”的呵护并不常见!

3. record 类的使用示例

普通类(代码很多):


public class Client {
    private final String name;
    private final int id;

    public Client(String name, int id) {
        this.name = name;
        this.id = id;
    }

    public String getName() { return name; }
    public int getId() { return id; }

    // equals, hashCode, toString 等。
}

Record 类(一行就够!):


public record Client(String name, int id) { }

用法:


public class Main {
    public static void main(String[] args) {
        Client client = new Client("Ivan", 123);
        System.out.println(client.name()); // Ivan
        System.out.println(client.id());   // 123
        System.out.println(client);        // Client[name=Ivan, id=123]
    }
}

请注意:
- 字段的访问方法名与字段名相同:name()id()
- 没有 setName()setId()——对象在创建后不可修改。

4. record 类的优势:更少代码,更少错误

代码更少——更快乐

既然一行就够,何必写 40 行?record 类能显著节省时间与精力,尤其是在包含大量 DTO 和 value 对象的大型项目中。

“按约定”的不可变性

  • record 类始终是 final 且不可变。
  • 对象无法被伪造或被意外修改。
  • 不会因为在意想不到的地方修改了对象状态而产生“诡异”的 bug。
  • 在多线程程序中可以安全使用(前提是所有字段也都是不可变的)。

equals/hashCode/toString 自动生成

无需手写比较、哈希计算和友好打印方法。这些都会被自动且正确地生成。


Client c1 = new Client("Anna", 42);
Client c2 = new Client("Anna", 42);

System.out.println(c1.equals(c2));                 // true
System.out.println(c1.hashCode() == c2.hashCode()); // true
System.out.println(c1);                             // Client[name=Anna, id=42]

非常适合在集合与键中使用

record 对象可以作为 HashMap 的键、HashSet 的元素等——都会正常工作,因为 equalshashCode 会考虑所有字段。


import java.util.HashMap;
import java.util.Map;

Map<Client, String> clients = new HashMap<>();
clients.put(new Client("Anna", 42), "VIP");

System.out.println(clients.get(new Client("Anna", 42))); // VIP

数据的显式描述

record 类的语法会直接告诉你它包含哪些数据、对象是不可变的。这让代码更清晰(对其他开发者也对半年后的你)。

5. 表格:普通类与 record 类的对比

普通类 Record 类
语法 代码很多 一行搞定
不可变性 需要显式编写 编译器保证
方法自动生成 是(equals, hashCode, toString)
是否可以添加字段 可以 仅限 record 组件
继承 可继承 始终是 final,不能被继承
在集合中的使用 需要正确实现相关方法 “开箱即用”

6. 扩展教学示例应用:使用 record 类

假设在你的教学银行应用中需要保存账户操作:日期、金额、操作类型(例如,“存入”或“取出”)。

在 Java 16 之前:


public class Transaction {
    private final LocalDate date;
    private final double amount;
    private final String type;

    public Transaction(LocalDate date, double amount, String type) {
        this.date = date;
        this.amount = amount;
        this.type = type;
    }

    public LocalDate getDate() { return date; }
    public double getAmount() { return amount; }
    public String getType() { return type; }

    // equals, hashCode, toString 等。
}

使用 Java 16 与 record:


import java.time.LocalDate;

public record Transaction(LocalDate date, double amount, String type) { }

用法:


Transaction t = new Transaction(LocalDate.now(), 100.0, "deposit");
System.out.println(t);         // Transaction[date=2024-06-01, amount=100.0, type=deposit]
System.out.println(t.amount()); // 100.0

7. 可视化:record 会生成什么

来看一个“展开”的 record 类(大致上编译器会生成这样的代码):


public final class Point extends java.zh.Record {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int x() { return x; }
    public int y() { return y; }

    @Override
    public boolean equals(Object o) { /* 按字段比较 */ }

    @Override
    public int hashCode() { /* 基于字段计算 */ }

    @Override
    public String toString() { /* 美观的输出 */ }
}

简而言之:何时应该使用 record

  • 当你需要一个包含一组数据的不可变对象。
  • 当你需要“靠谱的” equals/hashCode/toString 且无需手写。
  • 当你在做 DTO、Value Object、二元组、三元组、颜色、点、区间、集合键等。

8. 使用 record 类时的常见错误

错误 1:试图添加 setter 或在创建后修改字段。
record 类不允许修改其字段。如果你尝试添加类似 setX(int x) 的方法,编译器会直接提示“不允许”。同样——尝试直接修改字段也不行。

错误 2:试图添加非静态字段。
在 record 类中只能声明组件(即 record 名称后括号中的字段)以及静态字段。不能添加普通的非静态字段——编译器不会允许这么做。

错误 3:将 record 用于可变逻辑。
record 类并不适用于具有可变状态的对象。如果你需要在创建后修改某些东西——请使用普通类。

错误 4:忘了 record 永远是 final。
record 类不能被继承,也不能作为父类。试图违反这一限制会导致编译错误。关键点——不要尝试“扩展” record:它被设计成封闭的不可变类型。

错误 5:忽视自动生成的方法。
如果你要重写 equalshashCodetoString,务必谨慎——不要破坏它们的契约,否则集合与比较将无法正常工作。

1
任务
JAVA 25 SELF, 第 22 级, 课程 0
已锁定
创建图书电子卡片 📚
创建图书电子卡片 📚
1
任务
JAVA 25 SELF, 第 22 级, 课程 0
已锁定
查看学生信息 🎓
查看学生信息 🎓
1
任务
JAVA 25 SELF, 第 22 级, 课程 0
已锁定
验证地图上城市的相同性 🗺️
验证地图上城市的相同性 🗺️
1
任务
JAVA 25 SELF, 第 22 级, 课程 0
已锁定
应用中的用户访问控制 🔑
应用中的用户访问控制 🔑
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION