本文详解如何借助 Lombok 的 @With 注解,为含集合字段(如 Set)的不可变值对象(@Value)生成语义清晰、线程安全的“with 方法”,实现单字段(尤其是集合)的原子级替换,避免 toBuilder() 在集合重置场景下的失效问题。
本文详解如何借助 lombok 的 `@with` 注解,为含集合字段(如 `set
在使用 Lombok @Value + @Builder(toBuilder = true) 构建不可变 DTO 时,开发者常期望通过 .toBuilder().skills(...).build() 替换整个集合,但实际运行中会发现:.skills(Collection) 并不会清空原有集合并赋新值,而是被 @Singular 的累积逻辑覆盖(即追加而非替换)。正如示例所示,即使显式调用 .skills(Collections.emptySet()),输出仍保留 "Java", "Spring" —— 这是因为 @Singular 仅提供 skill(String)(单元素追加)和 skills(Collection)(批量追加),不支持“覆盖式重设”。
此时,@With 是更精准、更符合不可变语义的解决方案。它为指定字段生成 withXxx(...) 方法,该方法返回一个全新实例,且对字段执行直接赋值替换(非合并),天然支持集合的整体更新。
✅ 正确用法:@With 替代 toBuilder() 处理集合重置
import lombok.Value;
import lombok.Builder;
import lombok.With;
import java.util.Set;
import java.util.Collections;
@Value
@Builder
public class Person {
String name;
// 关键:为 skills 字段启用 @With,同时保留 @Singular 用于构建时的便捷添加
@With
@Singular
Set<string> skills;
}</string>
使用示例:
Person john = Person.builder()
.name("John")
.build();
System.out.println(john);
// 输出:Person(name=John, skills=[])
// ✅ 使用 withSkills() 直接替换整个集合(深拷贝语义)
Person johnUpgraded = john.withSkills(Set.of("Java", "Spring"));
System.out.println(johnUpgraded);
// 输出:Person(name=John, skills=[Java, Spring])
// ✅ 精准清空:传入空集合,彻底替换,无残留
Person johnDowngraded = johnUpgraded.withSkills(Collections.emptySet());
System.out.println(johnDowngraded);
// 输出:Person(name=John, skills=[])
⚠️ 重要注意事项:
- @With 默认仅对非 final 字段生效。由于 @Value 自动生成 final 字段,需显式禁用 final 修饰或改用 @Data + 手动保证不可变性;更推荐方案是:移除 @Value,改用 @Data + @Builder + @With,并通过构造器/@AllArgsConstructor 控制实例创建入口,确保字段逻辑不可变。
- 若坚持使用 @Value,可配合 lombok.config 全局启用:
lombok.with.flag = true(强制为所有字段生成 with 方法)
或在字段上显式标注:@With(AccessLevel.PUBLIC)。- @With 生成的是浅拷贝语义的不可变更新:它复制对象本身并替换目标字段引用,但不递归克隆字段内部对象(如 skills 中的 String 是不可变的,安全;若为自定义对象集合,则需确保其自身不可变或已深拷贝)。
? 对比总结:何时选 @With?何时选 toBuilder()?
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 单字段整体替换(尤其集合清空/全量更新) | withXxx(newCollection) | 语义明确、无副作用、代码扁平 |
| 多字段增量修改 + 链式构建 | toBuilder().field1().field2().build() | 适合初始化后微调多个属性 |
| 嵌套对象深层更新(如 Person → Address → City) | 组合 withAddress(address.withCity("Beijing")) | @With 天然支持链式嵌套,比多层 toBuilder() 更简洁 |
综上,当你的核心诉求是安全、可控、可读地替换不可变对象中的集合属性时,@With 不仅可行,更是 Lombok 生态中针对该问题的首选惯用模式。它让不可变更新从“规避陷阱”变为“直觉编码”,显著提升领域模型的表达力与维护性。











