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 还支持 SetMap 等其他集合类型。

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 对英文名词复数形式的智能识别:

✅ 能识别的常见规则:

  • tagstag
  • boxesbox
  • grassesgrass

❌ 无法识别的情况:

  • 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


原始标题:Using the @Singular Annotation with Lombok Builders | Baeldung