使用lombok从现有对象构建一个新对象

229

假设我有一个使用lombok注解的类,如下所示:

@Builder
class Band {
   String name;
   String type;
}

我知道我能做到:

Band rollingStones = Band.builder().name("Rolling Stones").type("Rock Band").build();
有没有一种简单的方法,可以使用现有对象作为模板创建 Foo 对象,并更改其中一个属性?例如:
```python foo = Foo() new_foo = create_from_template(foo, {'property_to_change': 'new_value'}) ```
其中 `create_from_template` 函数会返回一个新的 `Foo` 对象,它和 `foo` 对象除了 `property_to_change` 属性不同之外,其他所有属性都相同。
Band nirvana = Band.builder(rollingStones).name("Nirvana");
我无法在Lombok文档中找到这个。
3个回答

477
你可以使用toBuilder参数给你的实例添加toBuilder()方法。
@Builder(toBuilder=true)
class Foo {
   int x;
   ...
}

Foo f0 = Foo.builder().build();
Foo f1 = f0.toBuilder().x(42).build();

根据文档

如果使用 @Builder 生成构建器以生成自己类的实例(除非将 @Builder 添加到不返回自己类型的方法中,否则始终如此),您可以使用 @Builder(toBuilder = true) 在自己的类中生成一个名为 toBuilder() 的实例方法;它创建一个新的构建器,其初始值都是该实例的所有值。

声明:我是 lombok 开发者。


25
还有一个更适合单字段更改的方法@WitherFoo f1 = f0.withX(42) - maaartinus
@maaartinus 我认为 @Wither 生成的 with* 方法总是返回一个新对象,而不是设置现有对象的字段。这样效率较低。 - MGhostSoft
7
@MGhostSoft,我显然假设创建一个新对象是目标。这很常见,因为越来越多地使用不可变对象。对于改变单个字段,最好使用 @Wither。对于超过两个字段的更改,使用 toBuilder 更优。请参见下面我的回答。 - maaartinus
5
对于零字段(即无任何更改的对象副本),@Wither 将不起作用,但.toBuilder().build()会起作用。 - M. Justin
1
完美的答案! - Gaurav
如果您正在使用一个库,而原始作者没有将标志设置为toBuilder,那该怎么办? - Alexis

77

有没有一种简单的方法,可以使用现有对象作为模板创建一个Foo对象,并更改其属性中的一个? (强调我的)

如果你真的只想更改一个属性,那么有一种更好、更有效的方法:

@With
class Band {
   String name;
   String type;
}

Band nirvana = rollingStones.withName("Nirvana");

凋零不会创建垃圾,但它可以更改单个字段。如果要更改多个字段,您可以使用

withA(a).withB(b).withC(c)....

并且会产生大量垃圾(所有中间结果),但是toBuilder更加高效和自然。

注意:早期版本的lombok使用了@Wither注解。请参见文档开头


1
会真的产生那么多垃圾吗?我认为这都是浅拷贝,除了你要替换的字段(前提是如果已经打算将对象变成不可变的话)。"垃圾"主要是被丢弃的顶层对象的引用(虽然我猜很多基本类型也可能会导致更多的垃圾)。 - jm0
1
@jm0 当然,这都是浅拷贝。我所说的“大量垃圾”指的是对withSomething进行一系列n次调用时会产生n-1个对象。一个对象的成本大约是几个字节加上每个引用4或8个字节加上每个原始类型1到8个字节。因此,我们每次调用大约会产生数十个字节的垃圾。通常情况下不是什么大问题。 - maaartinus
我认为现代JVM编译器会对这些对象使用逃逸分析和栈分配优化。它会发现中间对象是方法作用域的,不会逃出该方法,然后将对象字段展开到堆栈上,然后使用传统优化将代码减少到单个构造函数调用,将活动值传递给逃逸的新对象。也就是说,它会生成与本地手工制作一样高效的代码。 - gary
如果我试图向克隆对象添加新属性,@With注解是否起作用? - Victor Cui

3

你可能还想使用com.fasterxml.jackson.databind.ObjectMapper来复制对象

@AllArgsConstructor
@Setter
class Band {
    String name;
    String type;
}

ObjectMapper objectMapper = new ObjectMapper(); //it's configurable
objectMapper.configure( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false );
objectMapper.configure( SerializationFeature.FAIL_ON_EMPTY_BEANS, false );

Band rollingStones = new Band("Rolling Stones", "Rock Band");

Band nirvana = objectMapper.convertValue( rollingStones, Band.class);
nirvana.setName("Nirvana");

这段代码可以很容易地封装在一些实用方法中,以便在整个项目中使用,例如ConvertUtils.clone(rollingStones, Band.class)


我尝试了你的示例并且它可以工作(在添加@NoArgsConstructor之后),但是在我的实际代码中,convertValue返回与其作为参数相同的实例。值得注意的区别是我的类继承自一个父类。 - Michal Krasny
Jackson 2.10之前的版本(于2019年9月发布)通常在此情况下为convertValue返回相同的实例。2.10版本返回一个新实例:“请注意,Jackson 2.9和2.10之间的行为略有不同,因此,尽管早期使用了一些优化来避免输入为目标类型的写入/读取循环,但从2.10开始始终执行完整处理。” - M. Justin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接