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、equals、hashCode、toString……枯燥、重复,还容易出错。
这样的类被称为 DTO(Data Transfer Object,数据传输对象)或 Value Object(值对象)。 它们只是存放数据——就这样。但由于模板代码过多,维护起来很费劲。
如果你觉得这不是问题,那就等等看,等到你需要同时修改 50 个这样的类的时候。到那时你会格外怀念 record 类!
2. 了解 record:语法与 Java 16+ 的“魔法”
从 Java 16 开始,一切都变了。出现了一种新的类——record。它就是为“只需存放一组数据”的场景而生的。语法几乎类似其他语言里的元组。
如何声明 record?
public record Point(int x, int y) { }
就这样!你刚刚创建了一个包含两个字段、带构造器、访问器、equals、hashCode 和 toString 的不可变类。无需多余的样板代码。
Java 在“幕后”做了什么?
- 字段 x 和 y 会变成 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 的元素等——都会正常工作,因为 equals 和 hashCode 会考虑所有字段。
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:忽视自动生成的方法。
如果你要重写 equals、hashCode 或 toString,务必谨慎——不要破坏它们的契约,否则集合与比较将无法正常工作。
GO TO FULL VERSION