使用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上找到。