您说得对,我之前的回复确实存在逻辑混乱和内容不连贯的问题。让我重新认真分析您的需求,为您提供一个结构清晰、由浅入深的优化版本。
Lombok @Builder 集合字段初始化"坑":空指针异常排查指南
一、问题现象:从正常代码到诡异异常
1.1 场景还原
周五晚上预生产环境突发异常,一个运行已久的商品改进功能突然报空指针错误。
正常的功能代码:
// 商品改进建议VO类
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JsProductImproveVo {
private Long parentSkuId;
private String parentSku;
// 声明时已初始化的集合字段
private Set<String> improves;
private List<Long> improveIds = new ArrayList<>();
// 添加改进建议的方法
public void addImprove(Long improveId, String improve) {
if (!this.improveIds.contains(improveId)) { // 💥 报错行
this.improveIds.add(improveId);
this.improves.add(improve);
}
}
}
1.2 错误复现
// 测试代码
public static void main(String[] args) {
// 使用Builder创建对象
JsProductImproveVo vo = JsProductImproveVo.builder()
.parentSkuId(1L)
.parentSku("F123")
.build();
vo.addImprove(1L, "改进建议1"); // 抛出NullPointerException
}
错误信息:
Exception in thread "main" java.lang.NullPointerException
at vo.JsProductImproveVo.addImprove(JsProductImproveVo.java:25)
二、问题分析:为什么声明初始化的集合为null?
2.1 正常对象创建流程
// 传统new方式创建对象
JsProductImproveVo vo = new JsProductImproveVo();
// 创建流程:
// 1. 分配内存空间
// 2. 执行字段默认初始化:improveIds = new ArrayList<>()
// 3. 调用构造方法
// 4. 得到完整对象
2.2 @Builder创建对象的隐藏问题
// Builder方式创建对象
JsProductImproveVo vo = JsProductImproveVo.builder().build();
// 实际执行流程:
// 1. 创建Builder实例,所有字段默认为null
// 2. 调用build()方法
// 3. Builder调用全参构造器:new JsProductImproveVo(null, null, null, null)
// 4. 💥 关键问题:构造器参数中的null覆盖了字段的默认初始化值
2.3 源码级分析
通过反编译工具查看Lombok生成的代码:
原始代码:
private List<Long> improveIds = new ArrayList<>();
Lombok生成的全参构造器:
public JsProductImproveVo(Long parentSkuId, String parentSku,
Set<String> improves, List<Long> improveIds) {
this.parentSkuId = parentSkuId;
this.parentSku = parentSku;
this.improves = improves;
this.improveIds = improveIds; // 💥 传入的null覆盖了new ArrayList<>()
}
Builder的build()方法:
public JsProductImproveVo build() {
// improveIds未被设置,传入null!
return new JsProductImproveVo(parentSkuId, parentSku, improves, improveIds);
}
三、解决方案:三种策略应对不同场景
3.1 方案一:@Builder.Default(推荐方案)
适用条件:Lombok版本 ≥ 1.16.16
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JsProductImproveVo {
private Long parentSkuId;
private String parentSku;
private Set<String> improves;
// 🎯 添加@Builder.Default注解
@Builder.Default
private List<Long> improveIds = new ArrayList<>();
public void addImprove(Long improveId, String improve) {
// 现在improveIds永远不会为null
if (!this.improveIds.contains(improveId)) {
this.improveIds.add(improveId);
if (this.improves == null) {
this.improves = new HashSet<>();
}
this.improves.add(improve);
}
}
}
验证修复效果:
JsProductImproveVo vo = JsProductImproveVo.builder().build();
System.out.println(vo.getImproveIds()); // 输出: [] (空列表,不是null!)
System.out.println(vo.getImproveIds().size()); // 输出: 0 (正常!)
3.2 方案二:防御性编程(兼容方案)
适用条件:任何Lombok版本
public void addImprove(Long improveId, String improve) {
// 🛡️ 防御性检查:确保集合不为null
if (this.improveIds == null) {
this.improveIds = new ArrayList<>();
}
if (this.improves == null) {
this.improves = new HashSet<>();
}
// ✅ 现在可以安全操作
if (!this.improveIds.contains(improveId)) {
this.improveIds.add(improveId);
this.improves.add(improve);
}
}
3.3 方案三:自定义Builder(高级方案)
@Builder(builderClassName = "CustomBuilder")
public class JsProductImproveVo {
private List<Long> improveIds = new ArrayList<>();
// 自定义builder方法
public static CustomBuilder builder() {
return new CustomBuilder().improveIds(new ArrayList<>());
}
}
四、深度理解:@Builder工作机制解析
4.1 @Builder的代码生成逻辑
当使用@Builder注解时,Lombok会生成以下代码:
- Builder内部类:包含所有字段的setter方法
- build()方法:调用全参构造器创建对象
- 全参构造器:直接赋值,不执行字段默认初始化
4.2 问题根源总结
| 操作方式 | 字段初始化流程 | 结果 |
|---|---|---|
new JsProductImproveVo() | 字段默认初始化 → 构造器 | ✅ improveIds = new ArrayList<>() |
JsProductImproveVo.builder().build() | Builder设置字段 → 全参构造器赋值 | ❌ improveIds = null |
五、最佳实践与团队规范
5.1 代码规范建议
// ✅ 推荐写法
@Builder
public class OrderVO {
@Builder.Default
private List<OrderItem> items = new ArrayList<>();
@Builder.Default
private Map<Long, OrderItem> itemMap = new HashMap<>();
}
// ❌ 避免的写法
@Builder
public class OrderVO {
private List<OrderItem> items = new ArrayList<>(); // 会被覆盖为null!
}
5.2 团队检查清单
- 所有使用
@Builder的类是否检查了集合字段? - 集合字段是否添加了
@Builder.Default注解? - 是否在代码审查中重点检查此类问题?
5.3 版本兼容性处理
// 兼容性写法:防御性编程 + @Builder.Default
@Builder.Default
private List<Long> improveIds = new ArrayList<>();
public void addImprove(Long improveId, String improve) {
// 双重保障
if (this.improveIds == null) {
this.improveIds = new ArrayList<>();
}
// ... 业务逻辑
}
六、总结
核心问题
@Builder通过全参构造器创建对象时,会覆盖字段声明时的默认初始化值,导致集合字段为null。
解决方案优先级
- 首选:
@Builder.Default注解(Lombok ≥ 1.16.16) - 备选:防御性编程(兼容所有版本)
- 高级:自定义Builder(复杂场景)
实践建议
- 新项目统一使用
@Builder.Default - 老项目逐步重构,添加防御性检查
- 团队编码规范中明确此要求
- 代码审查时重点检查
通过理解@Builder的工作原理和采取适当的防护措施,可以完全避免这类空指针异常问题。

384

被折叠的 条评论
为什么被折叠?



