Java Record与Sealed Classes
约 1989 字大约 7 分钟
javarecordsealed
2025-03-16
概述
record(JDK 16正式)和 sealed classes(JDK 17正式)是Java现代化进程中的两个重要特性。Record 提供了不可变数据载体的简洁语法,Sealed Classes 限定了类的继承层次,两者结合 Pattern Matching 可以实现类似代数数据类型(ADT)的表达能力。
Record
基本语法
// 传统POJO需要大量样板代码
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 x() { return x; }
public int y() { return y; }
@Override public boolean equals(Object o) { ... }
@Override public int hashCode() { ... }
@Override public String toString() { ... }
}
// Record一行搞定
public record Point(int x, int y) {}编译器自动生成:
private final字段- 全参构造方法(canonical constructor)
- 访问器方法(
x(),y(),不是getX()) equals()基于所有组件hashCode()基于所有组件toString()包含类名和所有组件
自定义构造方法
// 紧凑构造方法(Compact Constructor)— 用于校验
public record Range(int min, int max) {
public Range {
// 不需要写参数列表
// this.min和this.max的赋值由编译器在最后自动完成
if (min > max) {
throw new IllegalArgumentException("min > max");
}
}
}
// 规范构造方法(Canonical Constructor)— 自定义赋值
public record Email(String value) {
public Email(String value) {
this.value = value.toLowerCase().trim();
}
}
// 自定义构造方法(必须委托给规范构造方法)
public record Point(int x, int y) {
public Point(int xy) {
this(xy, xy); // 必须调用规范构造方法
}
}Record的限制
public record Person(String name, int age) {
// 可以:声明静态字段和方法
private static final String DEFAULT_NAME = "Unknown";
public static Person unknown() { return new Person(DEFAULT_NAME, 0); }
// 可以:声明实例方法
public String greeting() { return "Hello, I'm " + name; }
// 可以:实现接口
// public record Person(String name, int age) implements Serializable {}
// 不可以:继承类(隐式继承java.lang.Record)
// 不可以:声明实例字段(只能有组件字段)
// 不可以:是abstract的
// 不可以:组件字段是可变的(始终final)
}| 允许 | 不允许 |
|---|---|
| 实例方法 | 实例字段(非组件) |
| 静态字段和方法 | 继承其他类 |
| 实现接口 | abstract修饰 |
| 自定义构造方法 | 可变组件(非final) |
| 注解(组件/字段/方法/参数) | — |
| 泛型 | — |
| 嵌套Record | — |
| 局部Record | — |
Record的实际应用
// 1. DTO / 值对象
public record UserDTO(Long id, String name, String email) {}
// 2. 方法返回多个值
public record ParseResult(int value, int nextIndex) {}
public ParseResult parse(String input, int start) {
return new ParseResult(42, start + 3);
}
// 3. Map的Key
record Coordinate(int x, int y) {} // equals/hashCode自动生成
Map<Coordinate, String> grid = new HashMap<>();
grid.put(new Coordinate(1, 2), "treasure");
// 4. 泛型Record
public record Pair<A, B>(A first, B second) {}
Pair<String, Integer> pair = new Pair<>("hello", 42);
// 5. 局部Record(方法内定义)
public List<String> processData(List<RawData> rawData) {
record Intermediate(String key, int score) {} // 局部Record
return rawData.stream()
.map(d -> new Intermediate(d.getKey(), computeScore(d)))
.filter(i -> i.score() > 50)
.map(Intermediate::key)
.toList();
}Sealed Classes
基本语法
sealed 类或接口限定了哪些类可以继承/实现它。
// 密封接口:只允许指定的类实现
public sealed interface Shape
permits Circle, Rectangle, Triangle {
}
// 允许的子类必须是final、sealed或non-sealed之一
public record Circle(double radius) implements Shape {} // final(record隐式final)
public record Rectangle(double w, double h) implements Shape {} // final
public sealed class Triangle implements Shape // sealed → 进一步限制
permits EquilateralTriangle, RightTriangle {}
public final class EquilateralTriangle extends Triangle {}
public final class RightTriangle extends Triangle {}
// non-sealed:放开限制,任何类都可以继承
public non-sealed class OpenShape implements Shape {}子类的三种修饰符
| 修饰符 | 含义 | 使用场景 |
|---|---|---|
final | 不能再被继承 | 终端节点 |
sealed | 继续限定子类 | 中间层级 |
non-sealed | 放开限制 | 扩展点 |
permits省略规则
如果所有permitted子类与sealed类在同一个编译单元(同一个文件)中,可以省略 permits 子句:
// Shape.java — 所有子类在同一文件中,自动推断
public sealed interface Shape {
record Circle(double radius) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
}Pattern Matching + Sealed Types
Sealed类与Pattern Matching结合,编译器可以进行穷举性检查。
switch表达式 + 模式匹配(JDK 21+)
public sealed interface Shape permits Circle, Rectangle, Triangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double w, double h) implements Shape {}
public record Triangle(double a, double b, double c) implements Shape {}
// 编译器知道所有可能的类型,确保穷举(不需要default分支)
public double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.w() * r.h();
case Triangle t -> {
double s = (t.a() + t.b() + t.c()) / 2;
yield Math.sqrt(s * (s - t.a()) * (s - t.b()) * (s - t.c()));
}
}; // 无需default,编译器知道这已经穷举了所有情况
}带守卫条件的模式匹配(JDK 21+)
public String describe(Shape shape) {
return switch (shape) {
case Circle c when c.radius() > 100 -> "大圆";
case Circle c -> "小圆,半径=" + c.radius();
case Rectangle r when r.w() == r.h() -> "正方形";
case Rectangle r -> "矩形";
case Triangle t -> "三角形";
};
}解构模式(Record Patterns, JDK 21+)
// Record解构 — 直接提取组件
public double area(Shape shape) {
return switch (shape) {
case Circle(var r) -> Math.PI * r * r;
case Rectangle(var w, var h) -> w * h;
case Triangle(var a, var b, var c) -> {
double s = (a + b + c) / 2;
yield Math.sqrt(s * (s - a) * (s - b) * (s - c));
}
};
}
// 嵌套解构
record Pair<A, B>(A first, B second) {}
record Point(int x, int y) {}
static void printPair(Pair<Point, Point> pair) {
switch (pair) {
case Pair(Point(var x1, var y1), Point(var x2, var y2)) ->
System.out.printf("(%d,%d) -> (%d,%d)%n", x1, y1, x2, y2);
}
}代数数据类型(ADT)模式
Record + Sealed Classes 可以表达类似函数式语言中的ADT。
// 表达式求值器 — 经典ADT示例
public sealed interface Expr {
record Num(double value) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
record Mul(Expr left, Expr right) implements Expr {}
record Neg(Expr expr) implements Expr {}
}
// 求值(递归模式匹配)
public static double calculate(Expr expr) {
return switch (expr) {
case Expr.Num(var v) -> v;
case Expr.Add(var l, var r) -> calculate(l) + calculate(r);
case Expr.Mul(var l, var r) -> calculate(l) * calculate(r);
case Expr.Neg(var e) -> -calculate(e);
};
}
// 使用
Expr expression = new Expr.Add(
new Expr.Num(1),
new Expr.Mul(new Expr.Num(2), new Expr.Num(3))
); // 1 + 2 * 3
System.out.println(calculate(expression)); // 7.0常见FAQ
Q: Record可以和Lombok一起用吗?
A: 可以,但大部分Lombok注解(如@Data、@Builder)与Record重复。Record本身已经提供了构造方法、访问器、equals/hashCode/toString。如果需要Builder模式,可以手动实现或使用 @Builder 配合自定义构造方法。
Q: Record适合做JPA Entity吗?
A: 不太适合。JPA Entity需要无参构造方法和setter(可变状态),而Record是不可变的。Record更适合做DTO、查询投影(@SqlResultSetMapping)或值对象。
Q: Sealed类可以跨包/模块吗?
A: JDK 17 要求permitted子类必须与sealed类在同一个模块中(如果是命名模块)。在无名模块中(传统classpath),permitted子类必须在同一个包中。
Q: 为什么要用sealed而不是包可见性?
A: 包可见性是隐式的约定,sealed是编译时强制的约束。sealed + switch模式匹配可以让编译器进行穷举检查,包可见性做不到这一点。
总结
Record 和 Sealed Classes 是Java向更现代、更表达力的语言演进的标志。Record 消除了数据类的样板代码,Sealed Classes 提供了受控的继承层次,两者结合 Pattern Matching 实现了类似代数数据类型的强大表达能力。在新项目中,推荐优先使用Record表示不可变数据,使用Sealed Classes定义封闭的类型层次。
贡献者
更新日志
9f6c2-feat: organize wiki content and refresh site setup于