从基础到高级:构建类型安全的不可变数据模型
一、记录类的设计哲学与演进
Java记录类(Record)是Java 14引入的预览特性,在Java 16中正式成为标准特性。
它代表了Java语言向更简洁、更安全的数据建模方向的重要演进,旨在解决传统Java Bean模式中的样板代码问题。
传统POJO(50+行代码)
- 私有字段 + getter/setter
- 手动实现equals/hashCode
- 手动实现toString
- 可变的(Mutable)
- 容易出错
记录类(1行代码)
- 自动生成组件方法
- 自动equals/hashCode
- 自动toString
- 不可变的(Immutable)
- 类型安全
二、记录类核心语法与高级特性
// 基础记录类定义
public record User(
String username,
String email,
LocalDateTime createdAt
) {
// 紧凑构造器(Compact Constructor)
public User {
Objects.requireNonNull(username, "用户名不能为空");
Objects.requireNonNull(email, "邮箱不能为空");
if (!email.contains("@")) {
throw new IllegalArgumentException("邮箱格式不正确");
}
// 字段初始化后自动完成
}
// 自定义方法
public String displayName() {
return username + " ";
}
// 静态工厂方法
public static User of(String username, String email) {
return new User(username, email, LocalDateTime.now());
}
// 静态字段
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[A-Za-z0-9+_.-]+@(.+)$");
}
// 嵌套记录类
public record Order(
String orderId,
List items,
Customer customer,
OrderStatus status
) {
// 本地记录类(Local Record)
public record OrderSummary(String orderId, BigDecimal total) {}
public OrderSummary toSummary() {
BigDecimal total = items.stream()
.map(OrderItem::subtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
return new OrderSummary(orderId, total);
}
}
// 泛型记录类
public record Response(
boolean success,
T data,
String message,
Instant timestamp
) {
// 类型安全的构建器模式
public static Response success(T data) {
return new Response(true, data, "操作成功", Instant.now());
}
public static Response error(String message) {
return new Response(false, null, message, Instant.now());
}
}
// 密封接口 + 记录类模式匹配
public sealed interface Shape
permits Circle, Rectangle, Triangle {
double area();
record Circle(double radius) implements Shape {
@Override
public double area() {
return Math.PI * radius * radius;
}
}
record Rectangle(double width, double height) implements Shape {
@Override
public double area() {
return width * height;
}
}
record Triangle(double base, double height) implements Shape {
@Override
public double area() {
return 0.5 * base * height;
}
}
}
// 模式匹配使用示例
public class ShapeCalculator {
public String describe(Shape shape) {
return switch (shape) {
case Shape.Circle c ->
String.format("圆形: 半径=%.2f, 面积=%.2f",
c.radius(), c.area());
case Shape.Rectangle r ->
String.format("矩形: 宽=%.2f, 高=%.2f, 面积=%.2f",
r.width(), r.height(), r.area());
case Shape.Triangle t ->
String.format("三角形: 底=%.2f, 高=%.2f, 面积=%.2f",
t.base(), t.height(), t.area());
};
}
}
语法特性解析:
- 紧凑构造器:在字段赋值前进行验证和转换
- 自动生成方法:equals(), hashCode(), toString(), 组件访问器
- 不可变性:所有字段都是final的
- 模式匹配友好:与Java模式匹配完美结合
- 序列化支持:自动实现Serializable接口
三、实战案例:电商领域数据建模
// 完整的电商领域模型示例
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
// 值对象:货币
public record Money(
BigDecimal amount,
String currency
) implements Comparable {
public Money {
Objects.requireNonNull(amount, "金额不能为空");
Objects.requireNonNull(currency, "货币不能为空");
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("金额不能为负数");
}
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("货币类型不匹配");
}
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(int quantity) {
return new Money(
this.amount.multiply(BigDecimal.valueOf(quantity)),
this.currency
);
}
@Override
public int compareTo(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("货币类型不匹配");
}
return this.amount.compareTo(other.amount);
}
}
// 值对象:地址
public record Address(
String country,
String province,
String city,
String street,
String postalCode
) {
public String fullAddress() {
return String.format("%s%s%s%s, 邮编: %s",
country, province, city, street, postalCode);
}
}
// 实体:商品
public record Product(
String productId,
String name,
String description,
Money price,
int stock,
List categories
) {
public Product withPrice(Money newPrice) {
return new Product(productId, name, description,
newPrice, stock, categories);
}
public Product reduceStock(int quantity) {
if (quantity > stock) {
throw new IllegalArgumentException("库存不足");
}
return new Product(productId, name, description,
price, stock - quantity, categories);
}
}
// 实体:订单项
public record OrderItem(
String itemId,
Product product,
int quantity,
Money unitPrice
) {
public OrderItem {
Objects.requireNonNull(product, "商品不能为空");
Objects.requireNonNull(unitPrice, "单价不能为空");
if (quantity <= 0) {
throw new IllegalArgumentException("数量必须大于0");
}
}
public Money subtotal() {
return unitPrice.multiply(quantity);
}
}
// 聚合根:订单
public record Order(
String orderId,
String customerId,
List items,
Address shippingAddress,
Address billingAddress,
OrderStatus status,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {
// 静态工厂方法
public static Order create(String customerId,
List items,
Address shippingAddress,
Address billingAddress) {
String orderId = "ORD-" + UUID.randomUUID().toString().substring(0, 8);
return new Order(
orderId,
customerId,
List.copyOf(items), // 防御性复制
shippingAddress,
billingAddress,
OrderStatus.CREATED,
LocalDateTime.now(),
LocalDateTime.now()
);
}
// 业务方法
public Money totalAmount() {
return items.stream()
.map(OrderItem::subtotal)
.reduce(Money::add)
.orElse(new Money(BigDecimal.ZERO, "CNY"));
}
public Order addItem(OrderItem newItem) {
List updatedItems = new ArrayList(items);
updatedItems.add(newItem);
return new Order(
orderId, customerId, updatedItems,
shippingAddress, billingAddress,
status, createdAt, LocalDateTime.now()
);
}
public Order updateStatus(OrderStatus newStatus) {
return new Order(
orderId, customerId, items,
shippingAddress, billingAddress,
newStatus, createdAt, LocalDateTime.now()
);
}
// 领域事件
public record OrderCreatedEvent(
String orderId,
String customerId,
Money totalAmount,
LocalDateTime occurredAt
) implements DomainEvent {
public static OrderCreatedEvent from(Order order) {
return new OrderCreatedEvent(
order.orderId(),
order.customerId(),
order.totalAmount(),
LocalDateTime.now()
);
}
}
}
// 枚举:订单状态
public enum OrderStatus {
CREATED, PAID, SHIPPED, DELIVERED, CANCELLED
}
// 服务层使用示例
public class OrderService {
private final OrderRepository orderRepository;
private final EventPublisher eventPublisher;
public OrderResult placeOrder(CreateOrderCommand command) {
// 验证业务规则
validateCommand(command);
// 创建订单聚合
Order order = Order.create(
command.customerId(),
command.items(),
command.shippingAddress(),
command.billingAddress()
);
// 保存聚合
orderRepository.save(order);
// 发布领域事件
eventPublisher.publish(Order.OrderCreatedEvent.from(order));
return new OrderResult(
order.orderId(),
order.totalAmount(),
order.status()
);
}
public record OrderResult(
String orderId,
Money totalAmount,
OrderStatus status
) {}
}
领域驱动设计最佳实践:
值对象
使用记录类表示Money、Address等值对象,确保不可变性和值语义
实体
通过with方法实现不可变实体的状态变更,保持引用透明性
领域事件
使用嵌套记录类表示领域事件,类型安全且自描述
四、与框架和库的集成
Spring Boot集成
// Spring MVC请求/响应体
public record ApiResponse(
boolean success,
T data,
String message,
Instant timestamp
) {}
// JPA实体(需要额外配置)
@Entity
public record UserEntity(
@Id @GeneratedValue
Long id,
String username,
String email,
@CreationTimestamp
LocalDateTime createdAt
) {
// JPA需要无参构造器
public UserEntity() {
this(null, null, null, null);
}
}
// Spring Data Repository
public interface UserRepository
extends JpaRepository {
// 记录类作为投影
record UserProjection(String username, String email) {}
@Query("select new com.example.UserProjection(u.username, u.email) " +
"from UserEntity u where u.id = :id")
UserProjection findProjectionById(Long id);
}
Jackson序列化
// 自动序列化/反序列化
public record UserDTO(
@JsonProperty("user_name")
String username,
@JsonProperty("email_address")
@JsonFormat(pattern = "yyyy-MM-dd")
String email,
@JsonIgnore
String password
) {
// 自定义反序列化逻辑
@JsonCreator
public static UserDTO create(
@JsonProperty("username") String username,
@JsonProperty("email") String email) {
return new UserDTO(username, email, null);
}
}
// 使用示例
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
UserDTO user = new UserDTO("john", "john@example.com", "secret");
String json = mapper.writeValueAsString(user);
// 输出: {"user_name":"john","email_address":"john@example.com"}
UserDTO deserialized = mapper.readValue(json, UserDTO.class);
性能优化建议:
缓存hashCode
对于大型记录类,考虑缓存hashCode值以提高性能
避免深度嵌套
深度嵌套的记录类可能导致equals/hashCode性能下降
合理使用数组
数组字段需要特殊处理,建议使用List等集合类型
五、迁移策略与兼容性
从传统POJO迁移到记录类:
// 迁移前:传统Java Bean
public class User {
private Long id;
private String username;
private String email;
// 构造器、getter、setter、equals、hashCode、toString...
// 总共约50行代码
}
// 迁移步骤1:转换为基本记录类
public record User(
Long id,
String username,
String email
) {
// 自动生成所有方法
}
// 迁移步骤2:添加验证逻辑
public record User(
Long id,
String username,
String email
) {
public User {
Objects.requireNonNull(username, "用户名不能为空");
Objects.requireNonNull(email, "邮箱不能为空");
if (!EMAIL_PATTERN.matcher(email).matches()) {
throw new IllegalArgumentException("邮箱格式不正确");
}
}
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[A-Za-z0-9+_.-]+@(.+)$");
}
// 迁移步骤3:添加业务方法
public record User(
Long id,
String username,
String email
) {
public User {
// 验证逻辑...
}
public String displayName() {
return username + " ";
}
public User withEmail(String newEmail) {
return new User(id, username, newEmail);
}
// 保持向后兼容的静态工厂方法
public static User of(Long id, String username, String email) {
return new User(id, username, email);
}
}
// 迁移步骤4:处理序列化兼容性
public record User(
Long id,
String username,
String email
) implements Serializable {
private static final long serialVersionUID = 1L;
// 为Jackson等序列化框架提供兼容性
@JsonCreator
public static User create(
@JsonProperty("id") Long id,
@JsonProperty("username") String username,
@JsonProperty("email") String email) {
return new User(id, username, email);
}
}
迁移注意事项:
- 确保所有使用方都能处理不可变对象
- 更新反射代码(记录类没有setter方法)
- 处理序列化/反序列化的兼容性
- 更新单元测试,利用记录类的值语义
- 考虑使用with方法替代setter进行状态变更
技术总结与最佳实践
Java记录类是现代Java开发中的重要工具,它通过减少样板代码、增强类型安全性和促进不可变性,
显著提高了代码质量和开发效率。正确使用记录类可以构建更健壮、更易维护的应用程序。
核心原则:
- 优先使用记录类表示值对象:如Money、Email、Address等
- 合理使用with方法:实现不可变对象的状态变更
- 结合密封接口:构建类型安全的代数数据类型
- 充分利用模式匹配:简化条件逻辑和类型判断
- 注意框架集成:正确处理序列化和持久化需求
不可变数据
领域建模
现代Java架构
// 添加交互功能:代码块折叠/展开
document.addEventListener(‘DOMContentLoaded’, function() {
const preElements = document.querySelectorAll(‘pre’);
preElements.forEach(pre => {
// 添加标题栏
const header = document.createElement(‘div’);
header.style.cssText = `
background: #374151;
color: #d1d5db;
padding: 8px 12px;
border-radius: 6px 6px 0 0;
font-family: monospace;
font-size: 12px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
user-select: none;
`;
const language = pre.parentElement.previousElementSibling?.textContent?.includes(‘代码’) ? ‘Java’ : ‘代码’;
header.innerHTML = `
${language} 代码示例
−
`;
// 包装pre元素
const wrapper = document.createElement(‘div’);
wrapper.style.cssText = `
margin-bottom: 1rem;
border-radius: 6px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
`;
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(header);
wrapper.appendChild(pre);
// 添加折叠功能
let isExpanded = true;
header.addEventListener(‘click’, function() {
isExpanded = !isExpanded;
pre.style.display = isExpanded ? ‘block’ : ‘none’;
header.querySelector(‘span:last-child’).textContent =
isExpanded ? ‘−’ : ‘+’;
});
// 添加复制功能
const copyButton = document.createElement(‘button’);
copyButton.textContent = ‘复制’;
copyButton.style.cssText = `
background: #4f46e5;
color: white;
border: none;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
cursor: pointer;
margin-left: 8px;
`;
header.querySelector(‘span:first-child’).appendChild(copyButton);
copyButton.addEventListener(‘click’, function(e) {
e.stopPropagation();
const code = pre.textContent;
navigator.clipboard.writeText(code).then(() => {
const originalText = copyButton.textContent;
copyButton.textContent = ‘已复制!’;
copyButton.style.background = ‘#10b981’;
setTimeout(() => {
copyButton.textContent = originalText;
copyButton.style.background = ‘#4f46e5’;
}, 2000);
});
});
});
// 添加回到顶部按钮
const backToTop = document.createElement(‘button’);
backToTop.textContent = ‘↑’;
backToTop.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
width: 40px;
height: 40px;
background: #0f766e;
color: white;
border: none;
border-radius: 50%;
font-size: 20px;
cursor: pointer;
display: none;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
`;
document.body.appendChild(backToTop);
window.addEventListener(‘scroll’, function() {
backToTop.style.display = window.scrollY > 500 ? ‘block’ : ‘none’;
});
backToTop.addEventListener(‘click’, function() {
window.scrollTo({ top: 0, behavior: ‘smooth’ });
});
});

