【问题标题】:Required arguments with a Lombok @BuilderLombok @Builder 的必需参数
【发布时间】:2015-07-05 07:08:11
【问题描述】:

如果我将@Builder 添加到课程中。生成器方法已创建。

Person.builder().name("john").surname("Smith").build();

我有一个需要特定字段的要求。在这种情况下,姓名字段是必需的,但姓氏不是。理想情况下,我想这样声明。

Person.builder("john").surname("Smith").build()

我不知道该怎么做。我尝试将 @Builder 添加到构造函数中,但没有成功。

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

【问题讨论】:

标签: java lombok


【解决方案1】:

你可以通过 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在这里是可选的。

【讨论】:

  • 能多解释一下就更好了。
  • 这个答案对我来说没有意义的是 hiddenBuilder() 没有隐藏......
  • @AkshatAgarwal 是什么让构建器方法隐藏?我 99.99% 确定 builderMethodName 只是更改了方法的名称 - 它不会将方法更改为隐藏。所以我仍然没有看到任何方法来实现具有必填字段的预期结果。
  • 我会告诉 Lombok 将构建器设为私有:@Builder(builderMethodName = "hiddenBuilder", access = AccessLevel.PRIVATE)
  • @Linus 似乎添加 AccessLevel.PRIVATE 也使构建器的所有方法都是私有的,并且认为它毫无用处。我弄错了吗?
【解决方案2】:

这是另一种方法:

@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 方法,使语法简洁自然。
  • 这种方法的问题是builder() 仍然可见,所以它真的 没有使参数成为必需的。无论如何,是的,我们还需要使用 @NonNull 注释——但这是一个运行时检查——如果一个人试图制作一个超级直观的对象,那肯定不够好。遗憾的是,没有办法在 lombok 中定制这种事情——即使我们可以将 builder() 方法设为私有,我们也可以创建自己的重载公共 builder(...) 方法带有必需的参数。
  • 显然在过去五年中发生了一些变化,因此 builder() 不再可用这种方法。与公认的 anwear 相比,这也没有给我一个非隐藏的 hiddenBuilder(),这使得这是我最喜欢的方法。
  • 感谢@TobiasGrunwald 的提醒——这使我的方法非常完美(尽管它确实让我有点担心任何仍在访问无参数构建器方法的人的向后兼容性!)
【解决方案3】:

我建议不要使用这种方法,因为您将难以将其一致地应用于其他对象。相反,您可以只使用 @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()

【讨论】:

  • 运行时错误与编译时错误。总是倾向于编译时错误!
  • @jax 他们不检查同样的事情。要求设置字段不会检查空值。无论您是否需要该字段,检查 null 将是运行时错误(在纯 Java 中)。
  • 嗯,我明白你现在的意思了,但你的方法也允许空值。最好提前了解对象契约,而不是让程序员猜测。看看《Effective Java 2nd edition》一书中的构建器模式。
  • 你也可以像 Person.builder(null).lastName("John").build();所以无论如何你仍然需要运行时检查。
  • @LakatosGyula,我同意,但我认为按照惯例,开发人员使用 null 调用所需字段的机会较小,使用常规构建器可能会出现更多错误,这是一种权衡,我会接受安东的回答。
【解决方案4】:

这是我解决问题的方法

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/

【讨论】:

【解决方案5】:

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

这是一个 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;
    }
}

【讨论】:

    【解决方案6】:

    让 Kevin Day 的 answer 更进一步:

    @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);
        }
    
    }
    

    这并不理想,但它提供了编译时强制,并且此类的调用者将只有一个构建器方法可供使用。

    【讨论】:

    • 很好,但是仍然可以通过调用.name() 来覆盖该值(错误地),所以我们会得到:Person.builder("john").surname("smith").name("mark").build();,当然最终我们会得到mark 不想要的john .我知道这是一个有点牵强的案例,但有可能,完美的解决方案可以避免这种情况。你认为有可能防御吗? (我现在正在处理)
    • @user3529850 如果您有必填字段,并且出于某种原因在您的构建器上下文中存在不变性问题,那么您最好以老式方式编写高度自定义的构建器,而不用 Lombok。请注意,在我给出的示例中,Person 是不可变的。您只能将名称设置一次。您正在做的是在 PersonBuilder 上多次设置名称,这并不是不可变的。
    • @user3529850 即使使用经典的构建器方法,您也始终可以在构造函数中传递 null...
    • @AmitGoldstein 显然,Java 没有内置的静态代码分析器来检测空值,这与 Builder 模式的 Lombok 实现无关
    【解决方案7】:

    如果你需要这个功能,你可以自己自定义builder类,仍然可以添加@BuilderAnnotation。

    @Builder
    public class Person {
    
        public static class PersonBuilder {
            private String name;
    
            private PersonBuilder() {
            }
    
            public PersonBuilder(final String name) {
                this.name = name;
            }
        }
    
        private static PersonBuilder builder() {
            return null; // or we can throw exception.
        }
    
        public static PersonBuilder builder(final String name) {
            return new PersonBuilder(clientId);
        }
    }
    

    【讨论】:

      【解决方案8】:

      User类为例,id是必填字段:

      @AllArgsConstructor(access = AccessLevel.PRIVATE) // required, see https://stackoverflow.com/questions/51122400/why-is-lombok-builder-not-compatible-with-this-constructor
      @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.builder("id-123").name("Tom").build; 等构建器初始化User 实例。使用 private no args 构造函数,您无法User user = new User();User user = new User("id-123");,因此您始终需要传递所需的参数id。请注意初始化的实例是不可变的。

      【讨论】:

      • 但我猜你仍然可以不带参数调用 UserBuilder 的默认构建器,没有人阻止它。
      【解决方案9】:

      结合@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);
        }
      }
      

      【讨论】:

      • 请注意,很遗憾,这不适用于@SuperBuilder,因为生成的构建器类也是抽象的。
      【解决方案10】:

      最佳实践:

      import lombok.Builder;
      import lombok.NonNull;
      
      @Builder(builderMethodName = "privateBuilder")
      public class Person {
          @NonNull
          private String name;
          private String surname;
      
          public static class PersonNameBuilder {
              public PersonBuilder name(String name) {
                  return Person.privateBuilder().name(status);
              }
          }
      
          public static PersonNameBuilder builder(String name) {
              return new PersonNameBuilder();
          }
      
          private static PersonBuilder privateBuilder(){
              return new PersonBuilder();
          }
      }
      

      用法:

      PersonNameBuilder nameBuilder = Person.builder();
      PersonBuilder builder = nameBuilder.name("John");
      Person p1 = builder.surname("Smith").build();
      
      // Or
      Person p2 = Person.builder().name("John").surname("Smith").build();
      

      【讨论】:

        【解决方案11】:

        尽管我很想拥有编译时验证功能,但该库的作者已经明确表示 the feature probably won't be added.

        所以我的看法是,拥有这样的东西。

        @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();
        

        不,没有验证。 但从图书馆用户的角度来看, 什么是必需的,什么不是,并且能够在不查看文档的情况下构建对象实例非常清楚。

        【讨论】:

          【解决方案12】:

          这是 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);
              }
          }
          

          【讨论】:

            猜你喜欢
            • 2020-03-02
            • 1970-01-01
            • 2021-06-21
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2019-06-15
            • 1970-01-01
            相关资源
            最近更新 更多