1. 概述

Project Lombok 通过提供自动生成常用代码的注解,减少了 Java 应用程序中的样板代码。

在这个教程中,我们将探讨此库提供的三种构造函数注解之间的差异。

2. 设置

为了突出这些差异,我们首先在依赖项中添加lombok:

<dependency>
    <groupId>org.projectlombok</groupId> 
    <artifactId>lombok</artifactId> 
    <version>1.18.30</version>
    <scope>provided</scope>
</dependency>

接下来,我们创建一个类作为演示的基础:

public class Person {
    private int age;
    private final String race;
    @NonNull
    private String name;
    private final String nickname = "unknown";
}

我们在 Person 对象中故意散布了各种不同的访问修饰符,每个构造函数注解处理方式不同。在接下来的每个部分,我们将使用这个类的不同名称的副本。

3. @AllArgsConstructor

顾名思义,**@AllArgsConstructor 注解会生成一个初始化所有对象字段的构造函数**。带有 @NonNull 标记的字段会在生成的构造函数中进行 null 检查。

让我们将注解添加到类中:

@AllArgsConstructor
public class AllArgsPerson {
    // ...
}

然后,我们在生成的构造函数中触发 null 检查:

@Test
void whenUsingAllArgsConstructor_thenCheckNotNullFields() {
    assertThatThrownBy(() -> {
        new AllArgsPerson(10, "Asian", null);
    }).isInstanceOf(NullPointerException.class)
      .hasMessageContaining("name is marked non-null but is null");
}

@AllArgsConstructor 提供了一个包含对象所有必要字段的 AllArgsPerson 构造函数。

4. @RequiredArgsConstructor

@RequiredArgsConstructor 注解会生成一个只初始化标记为 final@NonNull 的字段的构造函数,前提是它们在声明时未被初始化

让我们更新我们的类以使用 @RequiredArgsConstructor:

@RequiredArgsConstructor
public class RequiredArgsPerson {
    // ...
}

对于 RequiredArgsPerson 对象,这将导致只有两个参数的构造函数:

@Test
void whenUsingRequiredArgsConstructor_thenInitializedFinalFieldsWillBeIgnored() {
    RequiredArgsPerson person = new RequiredArgsPerson("Hispanic", "Isabela");
    assertEquals("unknown", person.getNickname());
}

由于我们初始化了 nickname 字段,尽管它是 final,但它不会成为生成构造函数参数的一部分。相反,它被视为其他非 final 字段和未标记为 NotNull 的字段。

与 @AllArgsConstructor 类似,@RequiredArgsConstructor 注解也会对带有 @NonNull 标记的字段执行 null 检查,如我们在单元测试中所示:

@Test
void whenUsingRequiredArgsConstructor_thenCheckNotNullFields() {
    assertThatThrownBy(() -> {
        new RequiredArgsPerson("Hispanic", null);
    }).isInstanceOf(NullPointerException.class)
      .hasMessageContaining("name is marked non-null but is null");
}

在使用 @AllArgsConstructor 或 @RequiredArgsConstructor 时,维护对象字段的顺序至关重要。例如,如果我们交换了 Person 对象中的 namerace 字段,由于它们具有相同的类型,编译器不会抱怨。然而,我们的库的现有用户可能会忽视调整构造参数的需要。

5. @NoArgsConstructor

通常,如果我们没有定义构造函数,Java 会提供一个默认的构造函数。同样,***@NoArgsConstructor 为一个类生成无参数构造函数,类似于默认构造函数**。我们指定 force 参数标志以避免因未初始化的 final 字段引起的编译错误:

@NoArgsConstructor(force = true)
public class NoArgsPerson {
    // ...
}

接下来,让我们检查未初始化字段的默认值:

@Test
void whenUsingNoArgsConstructor_thenAddDefaultValuesToUnInitializedFinalFields() {
    NoArgsPerson person = new NoArgsPerson();
    assertNull(person.getRace());
    assertEquals("unknown", person.getNickname());
}

与其它字段不同,nickname 字段没有收到 null 的默认值,因为我们是在声明时初始化它的。

6. 多个注解的使用

在某些情况下,不同的需求可能需要使用多个注解。例如,如果我们更喜欢提供一个静态工厂方法,但仍然需要一个默认构造函数以与外部框架(如[JPA](/jpa-no-argument-constructor-entity-class))兼容,我们可以使用两个注解:

@RequiredArgsConstructor(staticName = "construct")
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
public class SpecialPerson {
    // ...
}

然后,我们可以用示例值调用我们的静态构造函数:

@Test 
void whenUsingRequiredArgsConstructorWithStaticName_thenHideTheConstructor() { 
    SpecialPerson person = SpecialPerson.construct("value1", "value2"); 
    assertNotNull(person); 
}

在这种情况下,尝试使用默认构造函数实例化将导致编译错误。

7. 比较总结

让我们用表格来概括我们讨论的内容:

注解

生成的构造函数参数

@NonNull 字段的 null 检查

@AllArgsConstructor

所有对象字段(除了静态和已初始化的 final 字段)

@RequiredArgsConstructor

final@NonNull 字段

@NoArgsConstructor

8. 结论

在这篇文章中,我们探讨了Project Lombok提供的构造函数注解。我们了解到,@AllArgsConstructor 初始化所有对象字段,而@RequiredArgsConstructor 只初始化 final@NotNull 字段。此外,我们发现@NoArgsConstructor 生成类似默认构造函数的构造器,并讨论了如何结合使用这些注解。

一如既往,所有示例的源代码可以在GitHub上找到。