1. 扩展 record 类:额外方法
可以在 record 中添加方法吗?
当然可以!Record 就像一套装修好的公寓:墙和地板已经固定,不能更改,但你完全可以按自己的喜好摆放家具。在 record 内可以声明普通方法、静态方法,甚至可以保存常量。这意味着业务逻辑不必放到单独的“工具”类中——也可以直接优雅地嵌入到 record 本身。
示例:计算两点之间距离的方法
假设我们有一个用于表示平面上点的 record:
public record Point(int x, int y) {
// 额外方法
public double distanceTo(Point other) {
int dx = this.x - other.x;
int dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
}
现在可以这样使用:
Point p1 = new Point(0, 0);
Point p2 = new Point(3, 4);
System.out.println(p1.distanceTo(p2)); // 5.0
如你所见,可以用自定义方法“丰富”record 类——这非常方便!
示例:静态方法
public record Rectangle(int width, int height) {
public int area() {
return width * height;
}
public static Rectangle square(int size) {
return new Rectangle(size, size);
}
}
现在可以通过一次调用就创建“正方形”:
Rectangle r = Rectangle.square(5);
System.out.println(r.area()); // 25
2. 紧凑构造器与数据校验
为什么需要“紧凑”构造器?
规范的 record 构造器会自动生成并把参数赋值给字段。但有时我们希望添加对输入数据的校验(例如禁止负坐标)。
在普通类中我们会这样写:
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
if (x < 0 || y < 0) throw new IllegalArgumentException();
this.x = x;
this.y = y;
}
// ...
}
在 record 类中可以声明紧凑构造器——无需重复参数列表,也无需显式给字段赋值(编译器会代劳)。
紧凑构造器的语法
public record Point(int x, int y) {
public Point {
if (x < 0 || y < 0) {
throw new IllegalArgumentException("Coordinates must be non-negative");
}
// 无需手写:this.x = x; this.y = y;
}
}
- 构造器的参数会自动与 record 的组件一致。
- this.x = x 和 this.y = y 的赋值会在构造器主体执行完毕(或成功退出)后由编译器自动完成。
- 如果抛出异常,对象将不会被创建。
带校验的示例
Point p1 = new Point(3, 5); // OK
Point p2 = new Point(-1, 2); // 将抛出 IllegalArgumentException!
可以声明“普通”构造器吗?
可以!如果需要创建参数列表不同或带有额外逻辑的构造器,请显式声明它:
public record Range(int from, int to) {
public Range(int size) {
this(0, size); // 调用主构造器
}
}
3. record 类的限制
与普通类不同,record 类有一系列限制。牢记这些可以避免编译错误带来的困惑。
只有组件——不能添加额外的非静态字段
在 record 类中无法声明新的非静态字段:
public record Person(String name, int age) {
// int id; // 编译错误!不能添加非静态字段。
}
可以声明静态字段和方法:
public record Person(String name, int age) {
public static final String SPECIES = "Homo sapiens";
}
Record 始终是 final
Record 类不能作为父类(不能从它继承),也不能显式继承其他类(除了隐式继承自 java.lang.Record)。这意味着 record 类始终是“终态”结构。
public record User(String login) { }
// public class Admin extends User {} // 错误:不能从 record 继承!
可以实现接口
Record 类可以实现接口:
public interface Printable {
void print();
}
public record Invoice(int amount) implements Printable {
@Override
public void print() {
System.out.println("金额: " + amount);
}
}
4. 示例:在实际场景中扩展 records
我们来看几个实际示例,展示额外方法和紧凑构造器如何让 record 类更有用。
带有计算方法的 record
public record Circle(double x, double y, double radius) {
public double area() {
return Math.PI * radius * radius;
}
public double distanceTo(Circle other) {
double dx = x - other.x;
double dy = y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
}
带校验的 record
public record Email(String value) {
public Email {
if (value == null || !value.contains("@")) {
throw new IllegalArgumentException("无效的 email: " + value);
}
}
}
现在无法创建无效的 email:
Email e1 = new Email("test@example.com"); // OK
Email e2 = new Email("not-an-email"); // 将抛出 IllegalArgumentException
带有额外静态方法的 record
public record Temperature(double celsius) {
public static Temperature fromFahrenheit(double fahrenheit) {
return new Temperature((fahrenheit - 32) * 5 / 9);
}
public double toFahrenheit() {
return celsius * 9 / 5 + 32;
}
}
用法:
Temperature t = Temperature.fromFahrenheit(98.6);
System.out.println(t.celsius()); // 37.0
System.out.println(t.toFahrenheit()); // 98.6
5. 紧凑构造器:细节与限制
何时使用紧凑构造器?
- 当需要校验数据正确性时(验证)。
- 当需要在保存前修改数值(例如四舍五入,或将字符串转换为大写)。
- 当希望避免重复参数列表时。
工作特性
- 在紧凑构造器中不能显式给组件赋值(this.x = ...)——否则会导致编译错误,因为编译器会在构造器主体之后自动完成赋值。
- 在紧凑构造器中不能修改参数名——它们始终与 record 的组件名一致。
示例:自动取整
public record Money(double amount) {
public Money {
amount = Math.round(amount * 100) / 100.0; // 四舍五入到分位
}
}
6. 实践:完善教学用应用
假设我们在做一个教学用的银行业务应用。我们有一个 record 类 Transaction,用于保存金额、转出方和接收方。
public record Transaction(String from, String to, double amount) {
public Transaction {
if (amount <= 0) throw new IllegalArgumentException("金额必须为正数");
if (from == null || to == null) throw new IllegalArgumentException("字段不能为 null");
}
public String description() {
return String.format("转账 %.2f:从 %s 到 %s", amount, from, to);
}
}
用法:
Transaction t = new Transaction("Alice", "Bob", 150.0);
System.out.println(t.description()); // 转账 150.00:从 Alice 到 Bob
试图创建无效的交易会触发错误:
Transaction t2 = new Transaction("Alice", "Bob", -10.0); // IllegalArgumentException
表格:record 类中能做与不能做
| record 类中可以做的 | record 类中不可以做的 |
|---|---|
| 普通方法 | 新增非静态字段 |
| 静态方法和字段 | 继承其他类 |
| 实现接口 | 作为其他类的父类 |
| 紧凑构造器与普通构造器 | 创建后修改组件 |
| 重写方法 | 使用 setter |
7. 在具有非标准主体的 record 类中常见错误
错误 1:尝试添加非静态字段。
新手常会尝试在 record 类中加入“额外的内部逻辑字段”——例如计数器或缓存。这行不通:编译器会立即报错。如果你需要保存额外状态,很可能 record 类并不适合。
错误 2:在紧凑构造器中忘记做校验。
如果你希望对象始终有效,请在紧凑构造器中做校验。不要指望“用户不会输入奇怪的东西”。
错误 3:尝试在创建后修改组件。
record 类的字段是 final——既不能直接修改,也不能通过方法修改。如果你需要可变结构,请使用普通类。
错误 4:在方法和构造器中重复编写相同逻辑。
有时会把校验逻辑和计算逻辑分别写在方法和构造器里,造成重复。更好的做法是在构造器中完成所有校验,而方法用于“纯粹”的业务逻辑。
错误 5:忽视继承方面的限制。
Record 类始终是 final——无法从它派生子类。如果你在设计需要子类的层次结构,请使用普通类。
GO TO FULL VERSION