使用Lombok @Builder时需要的必选参数

104
如果我在一个类中添加 @Builder,那么将会创建一个构建器方法。
Person.builder().name("john").surname("Smith").build();

我有一个需求,其中一个特定的字段是必需的。在这种情况下,名字字段是必需的,但姓氏不是。理想情况下,我希望像这样声明它。
Person.builder("john").surname("Smith").build()

我无法弄清如何做到这一点。我尝试将@Builder添加到构造函数中,但没有成功。

@Builder
public Person(String name) {
    this.name = name;
}

Lombok在GitHub上有一个未解决的问题,链接为https://github.com/rzwitserloot/lombok/issues/1043。 - lennykey
由于您需要访问实际的构建器源代码,因此它可能无法与lombok一起使用,但是如果您确实可以访问,则可以尝试我开发的插件来解决这些问题:https://github.com/banterly91/Java-Builder-Guided-Completion-Intellij-Plugin - dragosb
14个回答

110

使用Lombok注解配置,您可以轻松完成此操作

import lombok.Builder;
import lombok.ToString;

@Builder(builderMethodName = "hiddenBuilder")
@ToString
public class Person {

    private String name;
    private String surname;

    public static PersonBuilder builder(String name) {
        return hiddenBuilder().name(name);
    }
}

然后就可以像这样使用它

Person p = Person.builder("Name").surname("Surname").build();
System.out.println(p);

当然这里的@ToString是可选的。


4
如果您能多解释一些会更好。 - Blip
57
我对这个答案感到困惑的是,hiddenBuilder()并不是隐藏的…… - Kevin Day
5
什么使builder方法隐藏?我99.99%确定builderMethodName只是更改方法的名称-它并不会将方法更改为隐藏状态。因此,我仍然看不到实现所需结果(即具有必填字段)的任何方法。 - Kevin Day
15
我会建议将Lombok的builder设为私有属性: @Builder(builderMethodName = "hiddenBuilder", access = AccessLevel.PRIVATE) - Linus
9
@Linus,似乎添加AccessLevel.PRIVATE会使生成器的所有方法都变为私有,这使得其相当无用。我错了吗? - Dean Gurvitz
显示剩余12条评论

62

我建议不采用这种方法,因为您将难以在其他对象上一致应用它。相反,您可以使用@ lombok.NonNull注释标记字段,Lombok将在构造函数和设置器中为您生成空值检查,这样如果未设置这些字段,则Builder.build()将失败。

使用构建者模式允许您非常清晰地识别要设置为哪个值的哪些字段。在您的示例中,对于名称字段已经失去了这种清晰性,如果正在构建具有多个必需字段的对象,则所有其他必需字段也将失去这种清晰性。请考虑下面的示例,您能否通过阅读代码来区分哪个字段是哪个?

Person.builder("John", "Michael", 16, 1987) // which is name, which is surname? what is 16?
    .year(1982) // if this is year of birth, then what is 1987 above?
    .build()

64
运行时错误和编译时错误。始终倾向于编译时错误! - jax
9
他们检查的不是同一件事情。要求设置一个字段并不检查空值。无论是否需要该字段,检查空值都将导致运行时错误(在纯Java环境下)。 - Anton Koscejev
1
我现在明白你的意思了,但是你的方法允许空值存在。最好直接说明对象合同,而不是让程序员猜测。建议查看《Effective Java第二版》中的构建器模式。 - jax
7
你也可以使用前面提到过的方式构建空字段,例如Person.builder(null).lastName("John").build(); 因此无论如何你仍需要进行运行时检查。 - Lakatos Gyula
2
@LakatosGyula,我同意你的观点,但是按照惯例,开发人员更不可能将必需字段设置为null,使用常规构建器可能会出现更多错误,这是一种权衡,我会选择Anton的答案。 - David Barda
显示剩余4条评论

35

进一步发挥Kevin Day的回答

@Builder
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE) // If immutability is desired
@ToString
public class Person {
    @NonNull // Presumably name cannot be null since its required by the builder
    private final String name;
    private final String surname;

    private static PersonBuilder builder() {
        return new PersonBuilder();
    }

    public static PersonBuilder builder(String name){
        return builder().name(name);
    }

}

这可能不是最理想的方案,但它提供了编译时执行和调用此类的调用者将只有一个构建器方法可用。


3
好的,但仍然有可能通过调用.name()来无意中覆盖该值,因此我们会得到:Person.builder("john").surname("smith").name("mark").build();,当然最终我们会得到mark而不是期望的john。我知道这是一个有点牵强的情况,但是完美的解决方案将避免这种情况。你认为有可能防范这种情况吗?(我现在正在处理这个问题) - user3529850
1
如果您在构建器的上下文中有必填字段和不可变性方面的顾虑,那么最好还是老式地编写高度定制的构建器,而不使用Lombok。请注意,在我给出的示例中,Person是不可变的。您只能设置名称一次。您正在对PersonBuilder多次设置名称,这并不是不可变的意图。 - The Gilbert Arenas Dagger
即使使用经典的构建器方法,您仍然可以在构造函数中传递null... - Amit Goldstein
@AmitGoldstein 显然,Java没有内置的静态代码分析器来检测空值,这与Lombok实现Builder模式无关。 - Kronen
为我做出了选择 - Frankie Drake

22

以下是另一种方法:

@Builder()
@Getter
@ToString
public class Person {

    private final String name;
    private final String surname;

    public static PersonBuilder builder(String name){
        return new PersonBuilder().name(name);
    }

    public static void main(String[] args) {
        Person p = Person.builder("John Doe")
                .surname("Bill")
                .build();
    }
}

我更喜欢你的方法,因为它只是重载了“builder”方法,使语法简洁自然。 - Jezor
10
这种方法的问题在于builder()仍然可见,所以它并没有真正使参数成为必需项。当然,我们需要使用@NonNull注释,但那只是一个运行时检查,如果想要创建超级直观的对象,这显然是不够好的。遗憾的是,在lombok中没有一种方式可以定制这种东西 - 即使我们可以让builder()方法变为私有的,我们仍然可以创建自己的公共builder(...)方法来设置必需的参数。 - Kevin Day
1
显然,在过去的五年中发生了一些变化,因此使用这种方法将不再可用builder()。与被接受的答案相反,这也没有给我一个非隐藏的hiddenBuilder(),这使得这成为我最喜欢的方法。 - Tobias Grunwald
感谢@TobiasGrunwald的提醒 - 这使得我的方法几乎完美(尽管这让我有点担心对于仍在访问无参数构建器方法的人的向后兼容性!) - Kevin Day

18

最简单的解决方案是对所有必需的值添加@lombok.NonNull。当必填字段未设置时,构建器将无法构建对象。

以下是一个JUnit测试,展示了所有组合的final@NonNull的行为:

import static org.junit.Assert.fail;

import org.junit.Test;

import lombok.Builder;
import lombok.ToString;

public class BuilderTests {
    @Test
    public void allGiven() {
        System.err.println(Foo.builder()
            .nonFinalNull("has_value")
            .nonFinalNonNull("has_value")
            .finalNull("has_value")
            .finalNonNull("has_value")
            .build());
    }

    @Test
    public void noneGiven() {
        try {
            System.err.println(Foo.builder()
                .build()
                .toString());
            fail();
        } catch (NullPointerException e) {
            // expected
        }
    }

    @Test
    public void nonFinalNullOmitted() {
        System.err.println(Foo.builder()
            .nonFinalNonNull("has_value")
            .finalNull("has_value")
            .finalNonNull("has_value")
            .build());
    }

    @Test
    public void nonFinalNonNullOmitted() {
        try {
            System.err.println(Foo.builder()
                .nonFinalNull("has_value")
                .finalNull("has_value")
                .finalNonNull("has_value")
                .build());
            fail();
        } catch (NullPointerException e) {
            // expected
        }
    }

    @Test
    public void finalNullOmitted() {
        System.err.println(Foo.builder()
            .nonFinalNull("has_value")
            .nonFinalNonNull("has_value")
            .finalNonNull("has_value")
            .build());
    }

    @Test
    public void finalNonNullOmitted() {
        try {
            System.err.println(Foo.builder()
                .nonFinalNull("has_value")
                .nonFinalNonNull("has_value")
                .finalNull("has_value")
                .build());
            fail();
        } catch (NullPointerException e) {
            // expected
        }
    }

    @Builder
    @ToString
    private static class Foo {
        private String nonFinalNull;

        @lombok.NonNull
        private String nonFinalNonNull;

        private final String finalNull;

        @lombok.NonNull
        private final String finalNonNull;
    }
}

8

这是我对问题的解决方案

import lombok.Builder;
import lombok.Data;
import lombok.NonNull;

@Data
@Builder(builderMethodName = "privateBuilder")
public class Person {
    @NonNull
    private String name;
    @NonNull
    private String surname;
    private int age;//optional

public static Url safeBuilder() {
    return new Builder();
}

interface Url {
    Surname name(String name);
}

interface Surname {
    Build surname(String surname);
}

interface Build {
    Build age(int age);
    Person build();
}

public static class Builder implements Url, Surname, Build {
    PersonBuilder pb = Person.privateBuilder();

    @Override
    public Surname name(String name) {
        pb.name(name);
        return this;
    }

    @Override
    public Build surname(String surname) {
        pb.surname(surname);
        return this;

    }

    @Override
    public Build age(int age) {
        pb.age(age);
        return this;
    }

    @Override
    public Person build() {
        return pb.build();
    }
    }
}

受这篇博客文章的启发:

https://blog.jayway.com/2012/02/07/builder-pattern-with-a-twist/


21
这就是我希望通过lombok为我生成的内容。 - okutane
@okutane 听起来好像没有发生:https://groups.google.com/d/msg/project-lombok/gjUAHljdSK0/CdizSESdEAAJ - Nick

3

User类为例,其中id字段是必需的:

@AllArgsConstructor(access = AccessLevel.PRIVATE) // required, see https://dev59.com/AFUK5IYBdhLWcg3w4jUv
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
@Getter
public class User {
    private String id;
    private String name;
    private int age;

    public static UserBuilder builder(final String id) {
        return new UserBuilder().id(id);
    }
}

你只能通过构建器来初始化 User 实例,比如 User user = User.builder("id-123").name("Tom").build;。由于私有的无参构造函数,你不能直接使用 User user = new User();User user = new User("id-123"); 来初始化实例,所以你总是需要传递必需的参数 id。请注意,初始化后的实例是不可变的。


1
但我想你仍然可以不带参数调用UserBuilder的默认构建器,没有人会阻止它。 - Shadman R

3

结合@Pawel的回答和Max的评论...

import lombok.Builder;
import lombok.ToString;

@Builder
public class Person {

  private String name;
  private String surname;

  public static PersonBuilder builder(String name) {
    return new PersonBuilder().name(name);
  }
}

2
请注意,不幸的是,这与@SuperBuilder不兼容,因为生成的构建器类也是抽象的。 - Marv

2
这是Pawel的回应启发,带有隐藏的生成器:

import lombok.Builder;
import lombok.ToString;

@Builder(builderMethodName = "")
@ToString
public class Person {

    private String name;
    private String surname;

    public static PersonBuilder builder(String name) {
        return new PersonBuilder().name(name);
    }
}

1
尽管我很希望具备编译时验证功能,但库的作者已经明确表示该功能可能不会被添加。因此,我的想法是采用类似这样的方式。
@Builder
public class Person {
  String name;
  Integer age;
  Optional optional;

  @Builder
  public class Optional {
    String surname;
    String companyName;
    String spouseName;
}

}

你可以像这样使用它
 Person p = Person.builder()
            .age(40)
            .name("David")
            .optional(Person.Optional.builder()
                    .surname("Lee")
                    .companyName("Super Company")
                    .spouseName("Emma")
                    .build())
            .build();

不,没有验证。

但从库的用户角度来看,很清楚需要什么,不需要什么,并且能够构建一个对象实例而不必查看文档。


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