1. 概述
Lombok 是一个非常好用的 Java 库,它能极大简化数据类的编写。其中 @Builder 注解 是 Lombok 的核心功能之一,可以自动生成用于创建不可变对象的 Builder 类。
不过,当我们在使用标准的 Lombok Builder 构建包含集合(Collection)属性的对象时,操作起来会略显笨拙。这时候,Lombok 提供了另一个注解 —— @Singular,它专门用来优雅地处理集合字段,让 Builder 更加灵活且符合最佳实践。
2. Builder 与集合
Builder 模式通过链式调用的方式,让我们可以轻松构建不可变对象。先来看一个基础示例:
@Getter
@Builder
public class Person {
private final String givenName;
private final String additionalName;
private final String familyName;
private final List<String> tags;
}
我们可以这样创建 Person
实例:
Person person = Person.builder()
.givenName("Aaron")
.additionalName("A")
.familyName("Aardvark")
.tags(Arrays.asList("fictional","incidental"))
.build();
这种方式虽然可行,但不够优雅。如果集合内容较多,要么内联写死,要么提前声明变量,都容易破坏代码的流畅性。而 @Singular 就是为了解决这个问题。
2.1. 使用 @Singular 处理 List
我们给 Person
类添加一个新的集合字段,并使用 @Singular
注解:
@Singular private final List<String> interests;
此时,Builder 会自动生成单个元素添加的方法(如 interest(...)
),我们可以逐个添加元素:
Person person = Person.builder()
.givenName("Aaron")
.interest("history")
.interest("sport")
.build();
Builder 内部会将这些元素收集到一个 List 中,并在调用 build()
时生成最终的不可变集合。
✅ 优点:
- 链式调用更自然
- 支持逐个添加元素
- 自动封装成不可变集合
2.2. 支持其他集合类型
除了 List
,@Singular
还支持 Set
和 Map
等其他集合类型。
Set 示例
@Singular private final Set<String> skills;
使用方式类似:
Person person = Person.builder()
.givenName("Aaron")
.skill("singing")
.skill("dancing")
.build();
⚠️ 注意:由于 Set
的特性,重复值会被自动去重。
Map 示例
@Singular private final Map<String, LocalDate> awards;
Map 的处理方式略有不同,Builder 会生成接受键值对的方法:
Person person = Person.builder()
.givenName("Aaron")
.award("Singer of the Year", LocalDate.now().minusYears(5))
.award("Best Dancer", LocalDate.now().minusYears(2))
.build();
同样地,如果多次添加相同的 key,只有最后一次生效。
3. @Singular 方法命名规则
细心的同学可能已经发现,Builder 为每个集合字段提供了两种方法:
- 一种是设置整个集合(如
.awards(...)
) - 另一种是逐个添加元素(如
.award(...)
)
这得益于 Lombok 对英文名词复数形式的智能识别:
✅ 能识别的常见规则:
tags
→tag
boxes
→box
grasses
→grass
❌ 无法识别的情况:
fish
→ ❌(因为fish
单复数相同)children
→ ❌(不规则复数)
遇到这种情况,我们需要手动指定单数形式:
@Singular("oneFish") private final List<String> fish;
然后就可以正常使用:
Sea sea = Sea.builder()
.grass("Dulse")
.grass("Kelp")
.oneFish("Cod")
.oneFish("Mackerel")
.build();
💡 小技巧:可以使用 "oneFish"
、"child"
等方式来自定义方法名,避免歧义。
4. 关于不可变性
不可变对象是指一旦创建就不能被修改的对象。在响应式编程和并发场景中尤为重要。
虽然 Builder 模式本身就是为了支持不可变对象设计的,但如果直接传入可变集合,仍然存在风险:
List<String> tags = new ArrayList<>();
tags.add("fictional");
tags.add("incidental");
Person person = Person.builder()
.givenName("Aaron")
.tags(tags)
.build();
// ❌ 危险操作!外部修改了集合内容
tags.clear();
tags.add("non-fictional");
⚠️ 上面这种写法会导致数据污染,破坏不可变性。
而使用 @Singular
则能规避这个问题:
✅ 使用 @Singular
后,Builder 会在 build()
时生成真正的不可变集合(如 Collections.unmodifiableList()
),从而保证安全性。
5. 总结
在这篇文章中,我们介绍了 Lombok 的 @Singular
注解如何帮助我们更优雅地处理集合类型的字段。它不仅提升了 Builder 的表达力和灵活性,还增强了不可变对象的安全性。
主要亮点包括:
✅ 支持 List
, Set
, Map
✅ 自动生成单元素添加方法
✅ 自动处理不可变性
✅ 支持自定义方法名以应对不规则复数
如果你经常使用 Lombok 的 Builder 模式,强烈建议尝试 @Singular
,会让你的代码更干净、更安全。
完整示例代码已上传至 GitHub:https://github.com/eugenp/tutorials/tree/master/lombok-modules/lombok