使用Lombok创建不可变对象的副本
1. 引言
Lombok 是一个库,它能帮助我们在编写Java应用时显著减少样板代码。
在这个教程中,我们将了解如何利用这个库,在只改变一个属性的情况下创建不可变对象的副本。
2. 使用方法
在处理设计上不允许setter的不可变对象时,我们可能需要与当前对象相似但只有一个属性不同的新对象。这可以通过Lombok的@With
注解实现:
public class User {
private final String username;
private final String emailAddress;
@With
private final boolean isAuthenticated;
//getters, constructors
}
这个注解背后会生成如下代码:
public class User {
private final String username;
private final String emailAddress;
private final boolean isAuthenticated;
//getters, constructors
public User withAuthenticated(boolean isAuthenticated) {
return this.isAuthenticated == isAuthenticated ? this : new User(this.username, this.emailAddress, isAuthenticated);
}
}
然后我们可以使用上述生成的方法来创建原始对象的修改副本:
User immutableUser = new User("testuser", "[email protected]", false);
User authenticatedUser = immutableUser.withAuthenticated(true);
assertNotSame(immutableUser, authenticatedUser);
assertFalse(immutableUser.isAuthenticated());
assertTrue(authenticatedUser.isAuthenticated());
此外,我们还可以选择对整个类进行注解,这将为所有属性生成withX()
方法。
3. 要求
为了正确使用@With
注解,我们需要提供一个包含所有参数的构造函数。如上面的例子所示,生成的方法需要这个构造函数来创建原始对象的克隆。
我们可以使用Lombok自己的@AllArgsConstructor
或@Value
注解来满足这个要求。或者,我们也可以手动提供这个构造函数,确保类中的非静态属性顺序与构造函数中的属性顺序匹配。
值得注意的是,如果@With
注解用于静态字段,则什么都不会做。这是因为静态属性不被视为对象状态的一部分。此外,Lombok会跳过以$
开头的字段的方法生成。
4. 进阶用法
让我们探讨使用此注解的一些高级场景。
4.1. 抽象类
我们可以在抽象类的字段上使用@With
注解:
public abstract class Device {
private final String serial;
@With
private final boolean isInspected;
//getters, constructor
}
然而,我们需要为生成的withInspected()
方法提供实现。这是因为Lombok不知道我们的抽象类的具体实现,无法为其创建克隆:
public class KioskDevice extends Device {
@Override
public Device withInspected(boolean isInspected) {
return new KioskDevice(getSerial(), isInspected);
}
//getters, constructor
}
4.2. 命名约定
如前所述,Lombok会跳过以$
开头的字段。但如果字段以字符开头,那么它会被大写,最后在生成的方法前加上with
。
另一方面,如果字段以下划线开头,那么生成的方法直接前缀为with
:
public class Holder {
@With
private String variableA;
@With
private String _variableB;
@With
private String $variableC;
//getters, constructor excluding $variableC
}
根据上述代码,我们看到只有前两个变量会有withX()
方法生成:
Holder value = new Holder("a", "b");
Holder valueModifiedA = value.withVariableA("mod-a");
Holder valueModifiedB = value.with_variableB("mod-b");
// Holder valueModifiedC = value.with$VariableC("mod-c"); not possible
4.3. 方法生成的例外情况
我们应该注意,除了以$
开头的字段外,如果类中已经存在与生成方法名称相同(忽略大小写)的方法,Lombok不会生成withX()
方法:
public class Stock {
@With
private String sku;
@With
private int stockCount;
//prevents another withSku() method from being generated
public Stock withSku(String sku) {
return new Stock("mod-" + sku, stockCount);
}
//constructor
}
在上述情况下,不会生成新的withSku()
方法。
另外,Lombok会在以下情况下跳过方法生成:
public class Stock {
@With
private String sku;
private int stockCount;
//also prevents another withSku() method from being generated
public Stock withSKU(String... sku) {
return sku == null || sku.length == 0 ?
new Stock("unknown", stockCount) :
new Stock("mod-" + sku[0], stockCount);
}
//constructor
}
我们可以注意到withSKU()
方法的命名不同。
基本上,Lombok会在以下情况下跳过方法生成:
- 方法名称与生成方法相同(忽略大小写)
- 存在的方法具有与生成方法相同的参数数量(包括可变参数)
4.4. 生成方法的空值检查
类似于其他Lombok注解,我们可以在使用@With
生成的方法中添加空值检查:
@With
@AllArgsConstructor
public class ImprovedUser {
@NonNull
private final String username;
@NonNull
private final String emailAddress;
}
Lombok会为我们生成以下带有所需空值检查的代码:
public ImprovedUser withUsername(@NonNull String username) {
if (username == null) {
throw new NullPointerException("username is marked non-null but is null");
} else {
return this.username == username ? this : new ImprovedUser(username, this.emailAddress);
}
}
public ImprovedUser withEmailAddress(@NonNull String emailAddress) {
if (emailAddress == null) {
throw new NullPointerException("emailAddress is marked non-null but is null");
} else {
return this.emailAddress == emailAddress ? this : new ImprovedUser(this.username, emailAddress);
}
}
5. 总结
在这篇文章中,我们学习了如何使用Lombok的@With
注解来生成具有单个属性变化的对象副本。
我们也了解了这种方法生成何时工作,以及如何在其基础上添加额外验证,如空值检查。
如往常一样,代码示例可在GitHub上找到。