Lombok中的默认值。如何通过构造函数和生成器初始化默认值。

98

我有一个对象

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo {
    private int id;
    private String nick;
    private boolean isEmailConfirmed = true;
}

我有两种方式来初始化它

UserInfo ui = new UserInfo();
UserInfo ui2 = UserInfo.builder().build();

System.out.println("ui: " + ui.isEmailConfirmed());
System.out.println("ui2: " + ui2.isEmailConfirmed());

这里是输出

ui: true
ui2: false

看起来构建器没有得到默认值。我在属性中添加了@Builder.Default注解,现在我的对象看起来是这样的。

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo { 
    private int id;
    private String nick;
    @Builder.Default
    private boolean isEmailConfirmed = true;
}

这里是控制台输出

ui: false
ui2: true

我该如何使它们两个都为true


1
看起来这是一个已知的问题(请参见 https://github.com/rzwitserloot/lombok/issues/1347)。 - Dan Oak
9个回答

78

我的猜测是,如果没有对代码进行去Lombok化处理的话,这是不可能做到的。但是为什么不直接实现你需要的构造函数呢?Lombok的目的就是让你的生活更轻松,如果某些东西无法与Lombok一起使用,那就老老实实地用传统方式来做吧。

@Data
@Builder
@AllArgsConstructor
public class UserInfo { 
    private int id;
    private String nick;
    @Builder.Default
    private boolean isEmailConfirmed = true;
    
    public UserInfo(){
        isEmailConfirmed = true;
    }
}

控制台输出:

ui: true
ui2: true

更新
截至01/2021,这个问题在Lombok中似乎已经得到修复,至少对于生成的构造函数是这样的。请注意,当您混合使用Builder.Default和显式构造函数时仍然存在类似的问题


4
注意!目前 @Builder.Default 自 v1.16.16 版本起已失效,它将会将你的字段初始化为 null,而不考虑在字段级别设置的默认初始化值……请参见此处问题 - DaddyMoe
解决方案并不完美,因为您需要两次初始化字段。一次在字段声明时,另一次在构造函数中。这使得代码容易出错。我曾经使用过这个 https://dev59.com/SlYN5IYBdhLWcg3wJFX8#50392165 - Marcin Kłopotek
1
为了使代码更少出错,我添加了一个JUnit测试来验证无参构造函数是否编写良好:assertThat(new UserInfo().equals(UserInfo.builder().build()).describedAs("some properties marked with @Builder.Default are not properly set in the no-arg constructor").isTrue(); - Julien Kronegg
修复后,如果您有一个生成的构造函数,可以使用它来使您的显式构造函数工作:@NoArgsConstructor @Builder class UserInfo { @Builder.Default boolean isEmailConfirmed = true; UserInfo(String custom) { this(); restOfConstructor(); } } - mjaggard

55

由于@Builder.Default注解存在问题, 我不会使用它。但是,你可以通过将@Builder注解从类级别移动到自定义构造函数来使用以下方法:

@Data
@NoArgsConstructor
public class UserInfo {
    
    private int id;
    private String nick;
    private boolean isEmailConfirmed = true;

    @Builder
    @SuppressWarnings("unused")
    private UserInfo(int id, String nick, Boolean isEmailConfirmed) {
        this.id = id;
        this.nick = nick;
        this.isEmailConfirmed = Optional.ofNullable(isEmailConfirmed).orElse(this.isEmailConfirmed);
    }
}

这样做可以确保:

  • isEmailConfirmed 字段只在一个地方初始化,使得代码更少出错且后期维护更容易
  • 无论使用构造器还是无参构造函数,UserInfo 类的初始化方式都相同

换句话说,条件成立:true

new UserInfo().equals(UserInfo.builder().build())

在这种情况下,无论如何创建对象,都是一致的。当您的类被映射框架或JPA提供程序使用时,特别重要,因为在您不通过构建器手动实例化它时,将在背后调用no-args构造函数来创建实例。

上述描述的方法非常相似,但它有一个主要缺点。您必须在两个地方初始化字段,这使得代码容易出错,因为您需要保持值一致。


2
由于Lombok有许多怪癖,如果您不了解这些怪癖可能会带来很多麻烦,而不是好处,因此您也可以尝试使用Immutables(https://immutables.github.io)或Autovalue(https://github.com/google/auto/blob/master/value/userguide/builders.md)。 - Marcin Kłopotek
3
所有其他工具都有一个重大缺点:它们会生成一个新类。它们肯定有更少的问题,因为它们只是使用众所周知(且丑陋)的 API 的注释处理器。Lombok必须以不同的方式工作,它确实有一些缺陷。不过,我从它刚开始使用就很满意。 - maaartinus
注释并非完全失效,但在处理非原始对象时存在问题。默认设置对我来说在版本 1.16.22 中使用 Boolean 是有效的。 - Simon Tower
1
对于任何想知道如何在具有@Singular注释时执行此操作的人,请查看此评论:https://github.com/rzwitserloot/lombok/issues/1347#issuecomment-455429294TLDR:将您的@Singular注释移动到新创建的私有构造函数中。 - kibowki
1
最新版本的Lombok似乎不再出现故障。 - David Conrad
@Builder.Default 仍然存在问题。 (参见 https://github.com/projectlombok/lombok/issues/2340)。 - Marcin Kłopotek

8

另一种方法是通过覆盖lombok的getter方法来定义您自己的getter方法:

@Data
@Builder
@AllArgsConstructor
public class UserInfo { 
    private int id;
    private String nick;
    private Boolean isEmailConfirmed;

    public Boolean getIsEmailConfirmed(){
      return Objects.isNull(isEmailConfirmed) ? true : isEmailConfirmed;
    }
}

11
在属性的获取方法中使用逻辑判断是不被鼓励的。想象一下这样的情况:您想在类中添加一个额外的方法,该方法取决于字段(isEmailConfirmed)。直接使用字段或调用获取器可能会让实现者感到困惑。值可能不同,这可能导致错误。 - Marcin Kłopotek
2
@SahilChhabra 不明确是不好的。除非你想让代码使用者更加困惑,否则“已弃用”具有特殊目的。最后,“假设”这是“你的类”,并且“你应该知道”不应使用此字段等等还有另一个严重问题...保持一致性,你的代码将向未来的开发人员和最终用户展示关注! - prash
@prash 你不能总是遵循所有最佳实践。有时在使用有缺陷的第三方代码时,你必须偏离规范。你可以选择不使用第三方代码,或者选择通过确保未来的开发人员知道使用情况(这里“Deprecated”可以很好地完成这项工作)来解决问题。这只是一种选择和观点。我很高兴你有自己的看法。另外,我建议你再次阅读关于“Deprecated”的用法 - https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/deprecation/deprecation.html - Sahil Chhabra
2
@prash 我同意他的答案更好。我只是提供了另一种方法。我同意它有像你提到的那样的问题,但这取决于使用情况。此外,我从未反对最佳实践。我只是说有时候你不能遵循每一个最佳实践(再次取决于使用情况)。 - Sahil Chhabra
这不就是 @Getter(lazy=true) 的存在意义吗? - Egor Hans
显示剩余2条评论

5

我的经验是,@Builder 最好是实例化类的唯一方式,因此最好与 @Value 配对使用,而不是 @Data

对于所有字段都可以以任何顺序进行可变操作的类,以及您想保留链式调用的类,请考虑将其替换为 @Accessors(chain=true)@Accessors(fluent=true)

@Data
@Accessors(fluent=true)
public class UserInfo {
    private int id;
    private String nick;
    private boolean isEmailConfirmed = true;
}

这样可以让你在代码中流畅地构建对象,避免不必要的Builder对象创建:
UserInfo ui = new UserInfo().id(25).nick("John");

3
这是我的方法:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder(toBuilder = true)
public class UserInfo { 
    private int id;
    private String nick;
    private boolean isEmailConfirmed = true;
}

并且,接下来:
UserInfo ui = new UserInfo().toBuilder().build();

我使用相同的方法,但是不使用:UserInfo ui = new UserInfo().toBuilder().build(); 我更喜欢使用:UserInfo ui = new UserInfo(); 用更少的代码实现同样的功能。 - Ali Ertugrul

2
在1.18.2版本中,@NoArgsConstructor@Builder都可以使用,但是并不完全有效。
具有一个或多个字段的构造函数将使所有其他默认初始化为null:new UserInfo("Some nick")将再次使isEmailConfirmed变为false。
我处理这个问题的方法是:
public UserInfo(String nick) {
  this();
  this.nick = nick;
}

这样做可以初始化所有默认字段,并得到预期的构造函数。

0

您可以创建一个静态的Builder类,并填充默认值:

@Data
@Builder(builderClassName="Builder")
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo {
    private int id;
    private String nick;
    private boolean isEmailConfirmed;
    public static class Builder{
          //Set defaults here
          private boolean isEmailConfirmed = true;
    }
}

0

在无参的构造函数中初始化属性

private boolean isEmailConfirmed = true;

转换为:

public class UserInfo {

    public UserInfo() {
        this.isEmailConfirmed = true;
    }

}

0

自定义构造函数和@Builder.Default可能永远无法一起使用。

框架作者希望避免@Builder的双重初始化。

我通过public static CLAZZ of(...)方法重复使用.builder()

@Builder
public class Connection {
    private String user;
    private String pass;

    @Builder.Default
    private long timeout = 10_000;

    @Builder.Default
    private String port = "8080";

    public static Connection of(String user, String pass) {
        return Connection.builder()
            .user(user)
            .pass(pass)
            .build();
    }

    public static Connection of(String user, String pass, String port) {
        return Connection.builder()
            .user(user)
            .pass(pass)
            .port(port)
            .build();
    }

    public static Connection of(String user, String pass, String port, long timeout) {
        return Connection.builder()
            .user(user)
            .pass(pass)
            .port(port)
            .timeout(timeout)
            .build();
    }
}

请查看相关讨论:https://github.com/rzwitserloot/lombok/issues/1347


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