Java语言在过去几年经历了翻天覆地的变化。从Java 14引入Records预览版,到Java 17正式发布Sealed Classes,再到Java 21的Pattern Matching for switch,一系列新特性正在彻底改变Java开发者编写数据驱动代码的方式。本文将深入探讨Record与Sealed Class这两个核心特性,从语法细节到实战应用,帮助你写出更简洁、更安全、更具表达力的Java代码。

一、Record:不可变数据载体的终极方案
在Java 14之前,我们要创建一个简单的数据载体类,需要编写大量样板代码:私有字段、构造器、getter、equals、hashCode、toString。即使使用IDE自动生成,代码依然冗长且容易出错——修改字段后忘记更新equals/hashCode是常见的Bug来源。
Record类型正是为了解决这个问题而诞生的。它是一种特殊形式的类,用来表示不可变数据。编译器会自动生成所有必要的方法,让你专注于数据本身。
1.1 Record基础语法
一个Record的声明极其简洁:
1 public record Point(int x, int y) {}
这一行代码等价于一个包含以下内容的传统类:
- 两个private final字段
1x
和
1y - 一个接收所有字段的规范构造器(canonical constructor)
- 字段的访问器方法(
1x()
和
1y(),注意不是
1getX())
- 基于所有字段的
1equals()
、
1hashCode()和
1toString()实现
让我们看一个更实际的例子——HTTP请求响应模型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 public record ApiResponse<T>(
int statusCode,
String message,
T data,
LocalDateTime timestamp
) {
// 自定义紧凑构造器,添加验证逻辑
public ApiResponse {
if (statusCode < 100 || statusCode > 599) {
throw new IllegalArgumentException("Invalid HTTP status code: " + statusCode);
}
if (timestamp == null) {
timestamp = LocalDateTime.now();
}
}
// 便捷工厂方法
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(200, "OK", data, LocalDateTime.now());
}
public static <T> ApiResponse<T> error(int code, String message) {
return new ApiResponse<>(code, message, null, LocalDateTime.now());
}
}
1.2 紧凑构造器与规范构造器
Record支持两种构造器形式。紧凑构造器(Compact Constructor)没有参数列表,编译器会自动将参数匹配到对应字段。它非常适合做参数验证和默认值设置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 public record User(String name, String email, int age) {
// 紧凑构造器 —— 参数列表省略,但可以修改值
public User {
Objects.requireNonNull(name, "Name cannot be null");
Objects.requireNonNull(email, "Email cannot be null");
if (age < 0 || age > 150) {
throw new IllegalArgumentException("Invalid age: " + age);
}
// 规范化数据
name = name.strip();
email = email.strip().toLowerCase();
}
// 自定义非规范构造器,必须委托给规范构造器
public User(String name, String email) {
this(name, email, 0);
}
}
关键要点:在紧凑构造器中赋值会在字段初始化之前执行,你可以修改参数的值,这些修改后的值会被用于字段初始化。这是与普通构造器的重要区别。
1.3 Record的实现细节与限制
Record虽然在语法上简洁,但它有明确的限制以保持其语义纯粹性:
| 特性 | Record | 普通类 |
|---|---|---|
| 隐式继承 | java.lang.Record | java.lang.Object |
| 声明字段 | ❌ 只能有组件字段 | ✅ 任意字段 |
| 声明实例字段 | ❌ 禁止 | ✅ 允许 |
| 声明静态字段 | ✅ 允许 | ✅ 允许 |
| 实现接口 | ✅ 允许 | ✅ 允许 |
| 继承其他类 | ❌ 禁止 | ✅ 允许 |
| 被继承 | ❌ final | 可配置 |
| 声明本地方法 | ❌ 禁止 | ✅ 允许 |
| 字段可变性 | 不可变(final) | 可配置 |
虽然不能声明实例字段,但你可以声明静态字段和静态方法,以及实例方法。这为工具方法提供了空间:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 public record Money(BigDecimal amount, Currency currency) {
// 静态字段
public static final Money ZERO_YUAN = new Money(BigDecimal.ZERO, Currency.getInstance("CNY"));
// 实例方法
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(int factor) {
return new Money(this.amount.multiply(BigDecimal.valueOf(factor)), this.currency);
}
}

二、Sealed Class:受控继承的类型安全利器
在Sealed Class出现之前,Java的类型继承体系要么完全开放(任何类都能继承),要么完全封闭(final类不允许继承)。Sealed Class填补了中间地带——允许继承,但只允许特定的类继承。
这对于领域建模至关重要。在函数式编程中,代数数据类型(ADT)是核心概念,Sealed Class让Java也能优雅地表达ADT。
2.1 Sealed Class基础
Sealed Class使用
1 | sealed |
修饰符声明,并通过
1 | permits |
子句指定允许的子类:
1
2
3
4
5
6 public sealed interface Shape
permits Circle, Rectangle, Triangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double width, double height) implements Shape {}
public record Triangle(double a, double b, double c) implements Shape {}
子类必须是以下三种之一:
- final —— 不可再被继承(record隐式为final)
- sealed —— 可以被继承,但必须声明自己的permits列表
- non-sealed —— 开放继承,任何人都能继承(回到传统模式)
这种设计确保了类型继承的层次结构在编译期就是已知的,这是Pattern Matching能安全工作的前提。
2.2 实战:支付系统领域建模
让我们用一个真实的支付系统来展示Sealed Class的威力。一个支付系统需要支持多种支付方式,每种方式有不同的验证逻辑和手续费计算方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86 public sealed interface PaymentMethod permits CreditCard, WeChatPay, Alipay, BankTransfer {
/**
* 验证支付方式是否有效
*/
boolean isValid();
/**
* 计算手续费
*/
BigDecimal calculateFee(BigDecimal amount);
}
public record CreditCard(
String cardNumber,
String holderName,
LocalDate expiryDate,
String cvv
) implements PaymentMethod {
@Override
public boolean isValid() {
return cardNumber != null && cardNumber.length() == 16
&& expiryDate.isAfter(LocalDate.now());
}
@Override
public BigDecimal calculateFee(BigDecimal amount) {
// 信用卡手续费:1.5% + 固定0.3元
return amount.multiply(BigDecimal.valueOf("0.015"))
.add(BigDecimal.valueOf("0.3"));
}
}
public record WeChatPay(
String openId,
boolean isVerified
) implements PaymentMethod {
@Override
public boolean isValid() {
return openId != null && isVerified;
}
@Override
public BigDecimal calculateFee(BigDecimal amount) {
// 微信支付:0.6%
return amount.multiply(BigDecimal.valueOf("0.006"));
}
}
public record Alipay(
String userId,
String accountType
) implements PaymentMethod {
@Override
public boolean isValid() {
return userId != null
&& Set.of("personal", "enterprise").contains(accountType);
}
@Override
public BigDecimal calculateFee(BigDecimal amount) {
// 支付宝:0.55%
return amount.multiply(BigDecimal.valueOf("0.0055"));
}
}
public record BankTransfer(
String bankCode,
String accountNumber,
String accountName
) implements PaymentMethod {
@Override
public boolean isValid() {
return bankCode != null && accountNumber != null;
}
@Override
public BigDecimal calculateFee(BigDecimal amount) {
// 银行转账:固定5元
return BigDecimal.valueOf("5.0");
}
}
2.3 Sealed Class的包组织策略
当所有子类都在同一个源文件中时,
1 | permits |
子句可以省略。但在实际项目中,通常需要分开文件。这时需要注意几个规则:
- 子类必须与sealed类在同一个模块中(未命名模块则在同一个包中)
- 子类必须在声明中直接声明自己是sealed类的子类
- 子类必须确实是sealed类的子类型(不能只是声明而不实现)
推荐的项目结构:
1
2
3
4
5
6 com.example.payment/
├── PaymentMethod.java // sealed interface
├── CreditCard.java // record implements PaymentMethod
├── WeChatPay.java // record implements PaymentMethod
├── Alipay.java // record implements PaymentMethod
└── BankTransfer.java // record implements PaymentMethod

三、Pattern Matching for switch:类型安全的模式匹配
Sealed Class真正发挥威力,是在与Pattern Matching for switch配合使用时。Java 21正式引入的switch模式匹配,让你能用类型模式(type pattern)精确匹配不同的子类型,编译器还能验证穷举性(exhaustiveness)——如果你遗漏了某个子类型,编译器会报错。
3.1 基础类型模式匹配
1
2
3
4
5
6
7
8
9 public String describePayment(PaymentMethod payment) {
return switch (payment) {
case CreditCard cc -> "信用卡支付 - 尾号" + cc.cardNumber().substring(12);
case WeChatPay wp -> "微信支付 - " + (wp.isVerified() ? "已认证" : "未认证");
case Alipay ap -> "支付宝" + ap.accountType() + "账户";
case BankTransfer bt -> "银行转账 - " + bt.bankCode();
// 不需要default分支!编译器知道所有子类型都已覆盖
};
}
注意:由于
1 | PaymentMethod |
是sealed的,编译器知道所有可能的子类型。当你覆盖了所有permits列表中的类型时,switch表达式就是穷举的,不需要
1 | default |
分支。这是Sealed Class + Pattern Matching组合的核心价值——编译器帮你保证类型安全。
3.2 带守卫条件的模式匹配
Java 21引入了带守卫条件(Guarded Pattern)的when子句,让模式匹配更加强大:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 public String assessRisk(PaymentMethod payment, BigDecimal amount) {
return switch (payment) {
case CreditCard cc when cc.expiryDate().isBefore(LocalDate.now().plusMonths(1))
-> "高风险 - 信用卡即将过期";
case CreditCard cc when amount.compareTo(BigDecimal.valueOf(50000)) > 0
-> "中风险 - 大额信用卡支付";
case CreditCard cc
-> "低风险 - 信用卡支付";
case WeChatPay wp when !wp.isVerified()
-> "高风险 - 微信未认证用户";
case WeChatPay wp when amount.compareTo(BigDecimal.valueOf(20000)) > 0
-> "中风险 - 微信大额支付";
case WeChatPay wp
-> "低风险 - 微信支付";
case Alipay ap when "enterprise".equals(ap.accountType())
-> "低风险 - 支付宝企业账户";
case Alipay ap
-> "低风险 - 支付宝个人账户";
case BankTransfer bt
-> "低风险 - 银行转账(T+1到账)";
};
}
3.3 嵌套模式匹配
Java 21支持嵌套模式匹配,可以在一个模式中同时解构多层对象。这在处理复杂嵌套数据时极其强大:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 public sealed interface DeliveryResult {
record Success(String trackingId, LocalDateTime estimatedDelivery) implements DeliveryResult {}
record Failed(String reason, boolean retryable) implements DeliveryResult {}
record Partial(String trackingId, List<String> missingItems) implements DeliveryResult {}
}
public sealed interface OrderStatus {
record Pending(LocalDateTime createdAt) implements OrderStatus {}
record Processing(DeliveryResult delivery) implements OrderStatus {}
record Completed(LocalDateTime deliveredAt) implements OrderStatus {}
record Cancelled(String reason) implements OrderStatus {}
}
// 嵌套模式匹配
public String getOrderSummary(OrderStatus status) {
return switch (status) {
case Pending(var time) ->
"订单待处理 - 创建于 " + time;
case Processing(DeliveryResult.Success(var id, var eta)) ->
"处理中 - 已发货,快递单号:" + id + ",预计到达:" + eta;
case Processing(DeliveryResult.Failed(var reason, var retryable)) ->
"处理中 - 发货失败:" + reason + (retryable ? "(可重试)" : "(不可重试)");
case Processing(DeliveryResult.Partial(var id, var items)) ->
"处理中 - 部分发货,缺少:" + String.join("、", items);
case Completed(var time) ->
"已完成 - 送达时间:" + time;
case Cancelled(var reason) ->
"已取消 - 原因:" + reason;
};
}
四、Record + Sealed Class + Pattern Matching综合实战
现在让我们把三个特性组合起来,构建一个完整的配置解析系统。这个系统能从不同来源加载配置,支持类型安全的配置访问和变更通知:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86 /**
* 配置值类型 —— 使用Sealed Interface + Record定义
*/
public sealed interface ConfigValue {
record StringValue(String value) implements ConfigValue {}
record IntValue(int value) implements ConfigValue {}
record DoubleValue(double value) implements ConfigValue {}
record BoolValue(boolean value) implements ConfigValue {}
record ListValue(List<ConfigValue> items) implements ConfigValue {}
record MapValue(Map<String, ConfigValue> entries) implements ConfigValue {}
}
/**
* 配置来源
*/
public sealed interface ConfigSource {
record EnvironmentVars() implements ConfigSource {}
record PropertiesFile(Path path) implements ConfigSource {}
record RemoteConfig(String url, String authToken) implements ConfigSource {}
}
/**
* 配置加载结果
*/
public sealed interface LoadResult {
record Success(ConfigSource source, Map<String, ConfigValue> values,
LocalDateTime loadedAt) implements LoadResult {}
record Failure(ConfigSource source, String error) implements LoadResult {}
record Partial(ConfigSource source, Map<String, ConfigValue> values,
List<String> warnings) implements LoadResult {}
}
/**
* 配置服务 —— 使用模式匹配实现类型安全访问
*/
public class ConfigService {
private final Map<String, ConfigValue> config;
public ConfigService(Map<String, ConfigValue> config) {
this.config = Map.copyOf(config); // 防御性拷贝
}
/**
* 类型安全的配置获取
*/
public Optional<String> getString(String key) {
return Optional.ofNullable(config.get(key))
.filter(cv -> cv instanceof ConfigValue.StringValue)
.map(cv -> ((ConfigValue.StringValue) cv).value());
}
/**
* 使用模式匹配的通用获取方法
*/
public Optional<Object> getValue(String key) {
return Optional.ofNullable(config.get(key)).map(cv ->
switch (cv) {
case ConfigValue.StringValue(var v) -> v;
case ConfigValue.IntValue(var v) -> v;
case ConfigValue.DoubleValue(var v) -> v;
case ConfigValue.BoolValue(var v) -> v;
case ConfigValue.ListValue(var v) -> v;
case ConfigValue.MapValue(var v) -> v;
}
);
}
/**
* 格式化配置值(展示嵌套模式匹配)
*/
public String formatValue(ConfigValue value) {
return switch (value) {
case ConfigValue.StringValue(var v) -> """ + v + """;
case ConfigValue.IntValue(var v) -> String.valueOf(v);
case ConfigValue.DoubleValue(var v) -> String.format("%.2f", v);
case ConfigValue.BoolValue(var v) -> v ? "true" : "false";
case ConfigValue.ListValue(var items) -> "[" +
items.stream().map(this::formatValue).collect(Collectors.joining(", ")) + "]";
case ConfigValue.MapValue(var entries) -> "{" +
entries.entrySet().stream()
.map(e -> e.getKey() + ": " + formatValue(e.getValue()))
.collect(Collectors.joining(", ")) + "}";
};
}
}

五、Record序列化与反序列化的最佳实践
Record与JSON框架的集成是日常开发中的高频场景。Jackson从2.12版本开始原生支持Record,但仍有一些细节需要注意:
5.1 Jackson配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 public record UserProfile(
String username,
String email,
List<String> roles,
LocalDateTime createdAt
) {
// Jackson需要无参构造器来反序列化?不!
// Jackson 2.12+ 直接支持Record,但需要正确配置
// 自定义反序列化时使用@JsonCreator
@JsonCreator
public UserProfile {
// 紧凑构造器中做验证和数据清洗
if (username == null || username.isBlank()) {
throw new IllegalArgumentException("Username is required");
}
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Valid email is required");
}
roles = List.copyOf(roles); // 防御性拷贝确保不可变性
}
}
// ObjectMapper配置
ObjectMapper mapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
// 使用
String json = mapper.writeValueAsString(user);
UserProfile parsed = mapper.readValue(json, UserProfile.class);
5.2 Record与JPA的集成陷阱
虽然Record作为JPA实体在技术上可行(Hibernate 6+支持),但通常不推荐。Record的设计哲学是不可变,而JPA实体的生命周期需要可变性(延迟加载、脏检查等)。更推荐的做法是:使用传统类作为JPA实体,Record作为DTO:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51 // JPA实体 —— 可变,适合ORM
@Entity
@Table(name = "users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String username;
@Column(nullable = false, unique = true)
private String email;
// JPA需要无参构造器
protected UserEntity() {}
// getters and setters...
}
// Record DTO —— 不可变,适合API传输
public record UserDto(
Long id,
String username,
String email
) {
// 从实体转换
public static UserDto fromEntity(UserEntity entity) {
return new UserDto(entity.getId(), entity.getUsername(), entity.getEmail());
}
}
// 在Service层使用
@Service
public class UserService {
private final UserRepository repository;
public List<UserDto> findAll() {
return repository.findAll().stream()
.map(UserDto::fromEntity)
.toList(); // Java 16+ toList()
}
public UserDto create(CreateUserRequest request) {
UserEntity entity = new UserEntity();
entity.setUsername(request.username());
entity.setEmail(request.email());
return UserDto.fromEntity(repository.save(entity));
}
}
六、性能考量与常见误区
6.1 Record的性能特征
Record在运行时与普通类几乎没有性能差异。编译器生成的equals/hashCode方法与手写的质量相当。但有几个细节值得注意:
- Record的toString()会输出所有字段值,对于包含大量数据或敏感信息的Record,建议重写toString()
- equals/hashCode基于所有字段,如果你的”逻辑相等”不包含所有字段,需要自定义实现
- Record可包含可变字段的引用——如
1List
或
1Map,虽然引用不可变,但内容可变。务必在构造器中做防御性拷贝
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 // ⚠️ 危险:内部List可被外部修改
public record DangerousRecord(List<String> items) {}
// ✅ 安全:防御性拷贝 + 紧凑构造器
public record SafeRecord(List<String> items) {
public SafeRecord {
items = List.copyOf(items); // 不可变拷贝
}
}
// 证明问题
List<String> mutableList = new ArrayList<>(List.of("a", "b"));
DangerousRecord dangerous = new DangerousRecord(mutableList);
mutableList.add("c"); // 危险!dangerous.items()现在包含"c"
List<String> anotherList = new ArrayList<>(List.of("a", "b"));
SafeRecord safe = new SafeRecord(anotherList);
anotherList.add("c"); // 安全!safe.items()仍然是["a", "b"]
6.2 Sealed Class的编译期优化
Sealed Class的permits信息在编译期就确定了,JVM可以利用这些信息进行优化。在switch模式匹配中,当编译器能验证穷举性时,不需要生成default分支的防御代码,这可以带来微小的性能提升。
更重要的是,Sealed Class让编译器在重构时提供更强的安全保障。当你添加一个新的子类型时,所有switch表达式都会报编译错误,强制你处理新的情况。这是相比于传统if-else instanceof链的巨大优势。
七、Spring Boot 3.x中的实际应用
Spring Boot 3.x全面拥抱Java 17+,Record和Sealed Class在Spring生态中有了更多用武之地:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64 @RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
// 使用Record作为请求DTO —— 自动验证
public record CreateOrderRequest(
@NotBlank String productName,
@Positive int quantity,
@NotNull PaymentMethod paymentMethod
) {}
// 使用Record作为响应DTO
public record OrderResponse(
Long id,
String productName,
BigDecimal totalAmount,
String status
) {}
@PostMapping
public ResponseEntity<OrderResponse> create(
@Valid @RequestBody CreateOrderRequest request
) {
Order order = orderService.createOrder(
request.productName(),
request.quantity(),
request.paymentMethod()
);
return ResponseEntity.ok(new OrderResponse(
order.getId(),
order.getProductName(),
order.getTotalAmount(),
describeStatus(order.getStatus())
));
}
// Sealed Class + Pattern Matching处理订单状态
private String describeStatus(OrderStatus status) {
return switch (status) {
case OrderStatus.Pending(var time) ->
"待处理(创建于" + time + ")";
case OrderStatus.Processing(var delivery) ->
"处理中 - " + formatDelivery(delivery);
case OrderStatus.Completed(var time) ->
"已完成(" + time + ")";
case OrderStatus.Cancelled(var reason) ->
"已取消(" + reason + ")";
};
}
private String formatDelivery(DeliveryResult delivery) {
return switch (delivery) {
case DeliveryResult.Success(var id, _) -> "已发货 " + id;
case DeliveryResult.Failed(var reason, _) -> "发货失败:" + reason;
case DeliveryResult.Partial(_, var items) -> "部分发货";
};
}
}
注意上面代码中使用了Java 21的未命名变量(
1 | _ |
)语法——当你不关心某个解构值时,可以用下划线占位。这让模式匹配代码更加简洁。
总结
Record、Sealed Class和Pattern Matching for switch三者的组合,为Java带来了函数式编程中代数数据类型的完整体验。Record负责简洁地定义不可变数据,Sealed Class负责精确控制类型层次,Pattern Matching负责优雅地处理不同类型。
三者协同工作的核心价值在于:让编译器替你保证类型安全。当你新增一个Sealed Class的子类型时,编译器会强制你在所有switch表达式中处理它;当你修改Record的字段时,equals/hashCode/toString自动更新。这种”编译即正确”的体验,是传统Java代码无法提供的。
如果你正在使用Java 17+,强烈建议在项目中逐步采用这些特性。从DTO和配置类开始使用Record,从状态机和结果类型开始使用Sealed Class,从简单的switch表达式开始使用Pattern Matching。你会发现代码变得更简洁、更安全、更具表达力——这正是现代化Java编程的魅力所在。
汤不热吧