问题现象大概如下面示例,Account
对象,有一个type
属性,且设置了默认值为0
,但由于使用了Builder
方式构建,导致默认值丢失。
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private Long id;
private String name;
private String type = "0";
}
class Main {
public static void main(String[] args) {
Account account = Account.builder().name("Li").id(1L).build();
System.out.println(account);
}
}
输出内容
Account(id=1, name=Li, type=null)
通过反编译可以看出,问题就在于build
方法。
public static class AccountBuilder {
private Long id;
private String name;
private String type;
AccountBuilder() {
}
public Account.AccountBuilder id(Long id) {
this.id = id;
return this;
}
public Account.AccountBuilder name(String name) {
this.name = name;
return this;
}
public Account.AccountBuilder type(String type) {
this.type = type;
return this;
}
public Account build() {
return new Account(this.id, this.name, this.type);
}
public String toString() {
return "Account.AccountBuilder(id=" + this.id + ", name=" + this.name + ", type=" + this.type + ")";
}
}
加上@Builder.Default
注解即可
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private Long id;
private String name;
@Builder.Default
private String type = "0";
}
反编译后发现(已过滤掉了部分无关代码内容),其实现方式就是通过$default$type
方法来完成默认值的赋值操作。
public class Account {
private Long id;
private String name;
private String type;
private static String $default$type() {
return "0";
}
public static Account.AccountBuilder builder() {
return new Account.AccountBuilder();
}
public Account(Long id, String name, String type) {
this.id = id;
this.name = name;
this.type = type;
}
public Account() {
this.type = $default$type();
}
public static class AccountBuilder {
private Long id;
private String name;
private boolean type$set;
private String type$value;
AccountBuilder() {
}
public Account.AccountBuilder id(Long id) {
this.id = id;
return this;
}
public Account.AccountBuilder name(String name) {
this.name = name;
return this;
}
public Account.AccountBuilder type(String type) {
this.type$value = type;
this.type$set = true;
return this;
}
public Account build() {
String type$value = this.type$value;
if (!this.type$set) {
type$value = Account.$default$type();
}
return new Account(this.id, this.name, type$value);
}
public String toString() {
return "Account.AccountBuilder(id=" + this.id + ", name=" + this.name + ", type$value=" + this.type$value + ")";
}
}
}
实际上@Builder使用还会遇到其他问题
比如如果没有@NoArgsConstructor
注解,就会缺少无参的构造方法,这严重违反了代码编写规范。
@Data
注解,会生成无参构造方法@Builder
注解,会生成全属性的构造方法,但没有无参构造方法@Data
和@Builder
一起使用,哪怕你手动添加了无参构造方法或者添加了@NoArgsConstructor
注解,最后再运行时依然会报错。@Builder、@Data、@AllArgsConstructor、@NoArgsConstructor
关于上面这些问题,你可以说是使用者本身对于@Builder
注解的理解不深,但笔者认为在实际的业务开发中,难免会因为各种各样的原因而导致问题产生,对于一个方法的使用,不单单是要要求使用者能够完全了解,更重要的是方法本身在使用时会不会出现让使用者容易忽视的条件。
替代方案
我相信大多数使用@Builder
主要是因为链式编程所带来的便捷性,实际上笔者更推荐使用@Accessors
,它同样可以实现链式编程,同时还是避免多创建一个Builder
对象,更重要的是可以避免@Builder
的坑。
@Accessors(chain = true)
@Data
public class Account {
private Long id;
private String name;
// @Builder.Default
private String type = "0";
}
class Main {
public static void main(String[] args) {
// Account account = Account.builder().name("Li").id(1L).build();
// System.out.println(account);
Account ac = new Account().setId(1L).setName("Li");
System.out.println(ac);
}
}