1. 比较 record 与 class:主要区别是什么?
在 Java 中,我们有两种主要方式来定义数据类型:普通类(class)和 record 类(record)。乍一看,两者都能存储并处理数据。但深入一点就会发现,它们的差异远比想象的多!
差异表: class vs record
| 特性 | 普通类(class) | Record 类(record) |
|---|---|---|
| 可变性 | 任意:字段可以是 final,也可以不是 | 不可变:所有字段都是 final |
| 继承 | 可以继承(extends),默认不是 final | 始终为 final,不能作为超类 |
| 字段 | 任意:静态/非静态、final 或 非 final、任意类型 | 只能有 record 组件(private final),外加静态字段 |
| Getter/Setter | 需要自己编写(或用 Lombok 生成) | 自动生成 getter(方法名与字段名相同),没有 setter |
| equals/hashCode/toString | 通常手写或生成(equals、hashCode、toString) | 基于所有组件自动生成 |
| 构造器 | 任意个,数量不限 | 一个主构造(涵盖所有组件),可添加紧凑构造器 |
| 接口 | 可以实现 | 可以实现 |
| 附加方法 | 任意 | 可以添加,但只能添加方法(不能添加字段) |
| 在集合中的使用 | 可以,但需正确实现 equals/hashCode | 非常适合作为键/值,相关实现已就绪 |
示例以便直观理解
普通类:
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) { /* ... */ }
@Override
public int hashCode() { /* ... */ }
@Override
public String toString() { /* ... */ }
}
Record 类:
public record Person(String name, int age) { }
就这样!一行代码——我们就能得到相同(甚至更好)的效果。而且不会冒着忘记实现某些重要方法的风险。
2. record 类的限制
record 并不只是“短语法”,而是一种独立的概念,并有自己的严格规则。我们来详细看看。
record 始终是 final
从定义上讲,record 类始终是 final。这意味着你不能从 record 派生子类:
public record Point(int x, int y) { }
// public class ColoredPoint extends Point { } // 编译错误!
如果你需要扩展行为,请使用普通类或组合(将 record 作为字段嵌入到类中)。
record 不能作为超类
record 类不能作为其他类的父类,它始终是 final。这很合乎逻辑:如果允许这样做,别人就可能添加可变字段——“不可变数据”的整体概念就会崩塌。
仅限 final 字段(组件)
record 的所有组件都在头部声明,并且默认是 private final。你不能在 record 的主体中添加非静态字段:
public record User(String login, String email) {
// int counter; // 错误!不能添加非静态字段
static int totalUsers = 0; // 可以,这是静态字段
}
没有 setter
record 类的组件没有 setter。任何试图添加类似 setX(int x) 的方法都没有意义:对象创建后你无法修改字段值。
public record Point(int x, int y) {
// public void setX(int x) { this.x = x; } // 错误:无法修改 final 字段
}
没有无参构造器
record 类始终只有一个主构造器,接收所有组件的值。不能在未提供全部数据的情况下创建 record:
Point p = new Point(1, 2); // OK
// Point p = new Point(); // 错误:没有无参构造器
没有非静态初始化块
record 类不能包含非静态初始化块(即写在方法之外的大括号代码):
public record User(String login) {
// { /* ... */ } // 错误:禁止使用非静态初始化块
}
继承方面的限制
record 类不能显式继承其他类(除了 java.lang.Record,它对我们是隐藏的,作为所有 record 的基类)。但可以实现接口!
public interface Printable {
void print();
}
public record Book(String title) implements Printable {
@Override
public void print() {
System.out.println("打印书籍: " + title);
}
}
不适合复杂业务逻辑
record 更关注数据而非行为。如果你的对象存在复杂逻辑、可变状态、“生命周期”或大量依赖,record 就不适合。应使用普通类。
3. 什么时候该使用 record 类?
- DTO(Data Transfer Object):在应用层之间、服务/微服务或 REST 控制器之间传递不可变数据(例如 JSON 响应)。
- 值对象(Value Object):仅由其值定义的对象。
- 集合中的键和值:当正确实现 equals 和 hashCode 很重要时(例如用于 HashMap 或 Set)。
- 计算结果:当需要从方法一次返回多个值时(例如,record Pair<T, U>(T first, U second))。
示例:REST 控制器的 DTO
public record UserDto(String login, String email) { }
现在可以放心地从控制器返回此类型的对象,而不必担心有人会修改其字段。
示例:HashMap 的键
public record Point(int x, int y) { }
Map<Point, String> pointNames = new HashMap<>();
pointNames.put(new Point(1, 2), "A");
pointNames.put(new Point(3, 4), "B");
// 一切都能正常工作:equals 和 hashCode 已经实现!
4. 何时不应使用 record 类
- 可变状态:如果对象创建后至少有一个字段需要改变。
- 复杂逻辑:如果对象有复杂行为、很多方法,或包含可变状态的嵌套对象。
- 继承:如果需要类层次结构、抽象基类或方法重写。
- 业务实体:例如在数据库中持久化并拥有唯一标识的对象。
示例:需要普通类的场景
public class Account {
private String id;
private int balance;
public Account(String id, int balance) {
this.id = id;
this.balance = balance;
}
public void deposit(int amount) { balance += amount; }
public void withdraw(int amount) { balance -= amount; }
// getters, setters, equals, hashCode, toString...
}
这里明显可以看到对象状态会发生变化——record 不适用.
5. 实践示例:在 record 与 class 之间做选择
示例 1:record —— 理想之选
public record Rectangle(int width, int height) {
public int area() {
return width * height;
}
}
- 矩形仅由宽和高定义。
- 创建后无需更改这些值。
- 可以添加实用方法 area()。
- 其余工作由 Java 自动完成。
示例 2:class —— 更佳选择
public class MutableRectangle {
private int width;
private int height;
public MutableRectangle(int width, int height) {
this.width = width;
this.height = height;
}
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int area() { return width * height; }
}
需要在创建后修改矩形尺寸?请使用普通类。
6. 使用 record 类时的常见错误
错误 1:尝试添加非静态字段。
record 类不允许在组件列表之外声明非静态字段。如果尝试这样做,编译器会报错。例如:
public record City(String name) {
// int population; // 错误!
}
错误 2:想要添加 setter。
record 不支持为组件提供 setter。对象创建后尝试修改字段值会导致编译错误。
错误 3:尝试继承 record 或让其他类从 record 继承。
record 始终是 final。不能从 record 继承,record 也不能继承其他类(除了隐藏的 java.lang.Record)。
错误 4:将 record 用于可变对象。
如果你计划在对象创建后修改其状态,record 并不适合!请使用普通类。
错误 5:忽略构造器限制。
record 类必须拥有一个接收所有组件值的构造器。不存在无参构造器!
GO TO FULL VERSION