前言
- hello,大家好,我是你们的老朋友 Lorin,本周在开发中使用 @Builder (@Builder 是一个注解,通常与 Lombok 这种 Java 代码生成工具一起使用,可以帮助简化 Java 类的构建器模式(Builder Pattern)的使用和生成)的时候出现了一个默认值丢失事件,顺便借这个机会研究了一下 @Builder ,特此分享给大家,先说结论:建议在日常开发中谨慎使用 @Builder,为什么呢?废话不多说,发车:
场景复现
- 先复现一下我开发中遇到的问题:
@Builder
@Getter
class Student implements {
private String num;
private String name;
private String address = "default";
}
public class TestExample {
public static void main(String[] args) {
Student student = Student.builder().name("Student").num("123").build();
System.out.println(JSON.toJSONString(student));
}
}
{"name":"Student","num":"123"}
- 我们会发现我们设置的默认值 address = "default" 丢失了,我大感疑惑,因为我也是第一次遇到,打开编译生成的 class 文件一看豁然开朗:
class Student {
private String num;
private String name;
private String address = "default";
Student(String num, String name, String address) {
this.num = num;
this.name = name;
this.address = address;
}
// 生成的 StudentBuilder 对象把我们的默认值丢失了
public static StudentBuilder builder() {
return new StudentBuilder();
}
public String getNum() {
return this.num;
}
public String getName() {
return this.name;
}
public String getAddress() {
return this.address;
}
public static class StudentBuilder {
private String num;
private String name;
private String address;
StudentBuilder() {
}
public StudentBuilder num(String num) {
this.num = num;
return this;
}
public StudentBuilder name(String name) {
this.name = name;
return this;
}
public StudentBuilder address(String address) {
this.address = address;
return this;
}
public Student build() {
return new Student(this.num, this.name, this.address);
}
public String toString() {
return "Student.StudentBuilder(num=" + this.num + ", name=" + this.name + ", address=" + this.address + ")";
}
}
}
解决问题
- 知道原因后解决当然很简单,lombok 提供了 @Builder.Default 来设置默认值:
@Builder
@Getter
class Student {
private String num;
private String name;
@Builder.Default
private String address = "default";
}
public class TestExample {
public static void main(String[] args) {
Student student = Student.builder().name("Student").num("123").build();
System.out.println(JSON.toJSONString(student));
}
}
{"address":"default","name":"Student","num":"123"}
// 来看看 @Builder.Default 是怎么实现的:
class Student {
private String num;
private String name;
private String address;
private static String $default$address() {
return "default";
}
Student(String num, String name, String address) {
this.num = num;
this.name = name;
this.address = address;
}
public static StudentBuilder builder() {
return new StudentBuilder();
}
public String getNum() {
return this.num;
}
public String getName() {
return this.name;
}
public String getAddress() {
return this.address;
}
public static class StudentBuilder {
private String num;
private String name;
private boolean address$set;
private String address$value;
StudentBuilder() {
}
public StudentBuilder num(String num) {
this.num = num;
return this;
}
public StudentBuilder name(String name) {
this.name = name;
return this;
}
public StudentBuilder address(String address) {
this.address$value = address;
this.address$set = true;
return this;
}
public Student build() {
String address$value = this.address$value;
// 如果没有设置该属性则使用默认值
if (!this.address$set) {
address$value = Student.$default$address();
}
return new Student(this.num, this.name, address$value);
}
public String toString() {
return "Student.StudentBuilder(num=" + this.num + ", name=" + this.name + ", address$value=" + this.address$value + ")";
}
}
}
我为什么建议不使用 @Builder
- 上面的问题只要知道原理就很好的解决了,那我为什么还建议不使用 @Builder 呢?我们都知道 @Builder 可以简化我们代码的生成,让我们轻松的使用构造器。但 @Builder 同样有很多的不足。
@Builder 的不足
- @Builder 生成的构造器不是完美的,它不能区分哪些参数是必须的,哪些是可选的。如果没有提供必须的参数,构造器可能会创建出不完整或者不合法的对象。
补充一点:很多人 喜欢 @Builder 和 @Data 搭配使用,包括我自己哈哈哈,这样会导致生成的构造器是可变的,这违反了构造器原理的,构造器应该是不可变的。
因此建议 @Builder 使用在一些不可变的对象中。
- @Builder 生成的构造器不能处理抽象类型的参数,它只能接受具体类型的对象,限制了灵活性和拓展性。
- 使用不当很容易报错,增加了使用的复杂性。
- 继承关系时,子类需要使用 @SuperBuilder
- 设置默认值需要使用 @Builder.Default
- 需要额外创建 Builder 对象。
@Builder 适用的场景
- 从上面我们可以看出,@Builder 不适合使用在短暂对象上,而是应该使用在长期、固定不变的对象上。
不使用 @Builder 我们如何实现对象构造
@Data + final 实现字段必填
- 下面是一个简单的示例:
@Data
class Student {
/**
* 设置为 final 构造必填
*/
private final String num;
/**
* 设置为 final 构造必填
*/
private final String name;
private int age;
private String address = "default";
Student(String num, String name) {
this.num = num;
this.name = name;
}
}
public class TestExample {
public static void main(String[] args) {
Student student = new Student("123", "小明");
student.setAge(12);
System.out.println(student);
}
}
升级版:使用 @Accessors 实现链式构造 + final 实现字段必填
- 上面的方法我们发现无法实现类似 @Builder 的链式构造,我们可以结合 @Accessors 实现链式构造
- @Accessors 是 Lombok 提供的一个注解,用于配置生成的 getter 和 setter 方法的样式和命名方式。
@Accessors 的定义
@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface Accessors {
// 与chain=true类似,区别在于getter和setter不带set和get前缀
boolean fluent() default false;
// 设置chain=true,生成setter方法返回对象,代替了默认的返回void
boolean chain() default false;
// set、get方法忽略指定的前缀(驼峰式)
// 注:必须所有字段都有前缀
String[] prefix() default {};
}
使用示例
// fluent = true 日常开发中我们一般使用这个配置就行
@Data
@Accessors(fluent = true)
class Student {
/**
* 设置为 final 构造必填
*/
private final String num;
/**
* 设置为 final 构造必填
*/
private final String name;
private int age;
private String address = "default";
public Student(String num, String name) {
this.num = num;
this.name = name;
}
}
public class TestExample {
public static void main(String[] args) {
Student student = new Student("123", "小明").age(1).address("北京");
System.out.println(JSON.toJSONString(student));
}
}
{"address":"北京","age":1,"name":"小明","num":"123"}
// chain = true
@Data
@Accessors(chain = true)
class Student {
/**
* 设置为 final 构造必填
*/
private final String num;
/**
* 设置为 final 构造必填
*/
private final String name;
private int age;
private String address = "default";
public Student(String num, String name) {
this.num = num;
this.name = name;
}
}
public class TestExample {
public static void main(String[] args) {
Student student = new Student("123", "小明").setAge(1);
System.out.println(JSON.toJSONString(student));
}
}
{"address":"default","age":1,"name":"小明","num":"123"}
// prefix = "prefix"
@Data
@Accessors(chain = true,prefix = "prefix")
class Student {
/**
* 设置为 final 构造必填
*/
private final String prefixNum;
/**
* 设置为 final 构造必填
*/
private final String prefixName;
private int prefixAge;
private String prefixAddress = "default";
public Student(String prefixNum, String prefixName) {
this.prefixNum = prefixNum;
this.prefixName = prefixName;
}
}
public class TestExample {
public static void main(String[] args) {
Student student = new Student("123", "小明").setAge(1).setAddress("北京");
System.out.println(JSON.toJSONString(student));
}
}
{"address":"北京","age":1,"name":"小明","num":"123"}
实现原理
- 同样我们可以查看编译后的 class 文件看一下背后是如何实现的(以上面的案例三为例):
class Student {
private final String prefixNum;
private final String prefixName;
private int prefixAge;
private String prefixAddress = "default";
public Student(String prefixNum, String prefixName) {
this.prefixNum = prefixNum;
this.prefixName = prefixName;
}
public String getNum() {
return this.prefixNum;
}
public String getName() {
return this.prefixName;
}
public int getAge() {
return this.prefixAge;
}
public String getAddress() {
return this.prefixAddress;
}
public Student setAge(int prefixAge) {
this.prefixAge = prefixAge;
return this;
}
public Student setAddress(String prefixAddress) {
this.prefixAddress = prefixAddress;
return this;
}
// 省略一些其他方法
}
总结
- @Builder 是一个好用的工具,但是我们不能滥用。在构建一些长期、固定不可变的对象时我们可以适当使用 @Builder 进行构建;当构建一些短暂存活的对象时我们可以尝试 使用 @Accessors 实现链式构造 + final 实现字段必填 的方式。
- 也许大多数朋友其实在日常开发中都是 @Builder 和 @Data 一把梭(包括我自己),但只有不断尝试、总结、尝试改变,才能成为更优秀的自己,水滴总有汇聚成大海的一天。
最后
- 如果文章对你有帮助,请点赞、收藏、关注、谢谢大家。