1. 写在前头
大家好,我是方圆
,最近读完了《Effective Java 第三版》
,准备把其中可供大家一起学习的点来分享出来。
这篇博客儿主要是关于建造者模式在创建对象时的应用
,这已经成了我现在写代码的习惯,它在灵活性和代码整洁程度上,都让我十分满意。以下的内容非常的长,也是我费尽心力去完成的一篇博客儿,从初次应用建造者模式,到发现Lombok方便的注解,最后深挖Lombok的源码,大家既可以简单的学会它的应用
,也可以从源码的角度来弄清楚它为什么是这样儿
,就看你有什么需求了!
那,我们开始吧!
2. Java Beans创建对象
先创建一个Student类做准备,包含如下五个字段,姓名,年龄,爱好,性别和介绍
public class Student {
private String name;
private Integer age;
private String hobby;
/**
* 性别 0-女 1-男
*/
private Integer sex;
/**
* 介绍
*/
private String describe;
}
2.1 最常见的创建对象方式
- 直接new一个对象,之后逐个set它的值,比如我们现在需要一个
芳龄23岁的男生叫小明
Student xm = new Student();
xm.setName("小明");
xm.setAge(23);
xm.setSex(1);
- 四行代码看着好多,我现在想让代码好看一些,一行就把这个对象创建出来,那就,
添加个构造函数呗
// Student中添加构造函数
public Student(String name, Integer age, Integer sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
// 一行一个小明
Student xm2 = new Student("小明", 23, 1);
这下看着是舒心多了,一行代替了之前的四行代码
- 又来新需求了,创建一个对象,只要年龄和姓名,不要性别了,
如果还要使用一行代码的话,我们又需要维护一个构造方法
// Student中添加构造函数
public Student(String name, Integer age) {
this.name = name;
this.age = age;
}
// 一行一个小明
Student xm3 = new Student("小明", 23);
两个构造方法,维护起来感觉还好…
- 但是,
需求接连不断
,“再给我来一个只有名字的小明!”,“我还要一个有名字,有爱好的小明”,“我还要…”
有没有发现点儿什么,也就是说,只要创建包含不同字段的对象,都需要维护一个构造方法
,五个字段最多维护“5 x 4 x 3 x 2 x 1...”
个构造方法,这才仅仅是五个字段,现在想想如果每打开一个实体类文件映入眼帘的是无数个构造方法,我就…
所以这个弊端很明显,Java Beans创建对象会让代码行数很多,一行set一个属性,不美观
,而采用了构造方法创建对象之后,又要对构造方法进行维护,代码量大增
,难道代码美观和少代码量不能兼得吗?
3. effective Java说:用建造者模式创建对象
我先直接把代码写好,再一点点给大家讲
public class Student {
private String name;
private Integer age;
private String hobby;
/**
* 性别 0-女 1-男
*/
private Integer sex;
/**
* 介绍
*/
private String describe;
// 注意这里添加了一个private的构造函数,建造者字段和实体字段一一对应赋值
private Student(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.hobby = builder.hobby;
this.sex = builder.sex;
this.describe = builder.describe;
}
// 静态方法创建建造者对象
public static Builder builder() {
return new Builder();
}
/**
* 采用建造者模式,每个字段都有一个设置字段的方法
* 且返回值为Builder,能进行链式编程
*/
public static class Builder {
private String name;
private Integer age;
private String hobby;
private Integer sex;
private String describe;
// 私有构造方法
private Builder() {
}
public Builder name(String val) {
this.name = val;
return this;
}
public Builder age(Integer val) {
this.age = val;
return this;
}
public Builder hobby(String val) {
this.hobby = val;
return this;
}
public Builder sex(Integer val) {
this.sex = val;
return this;
}
public Builder describe(String val) {
this.describe = val;
return this;
}
public Student build() {
return new Student(this);
}
}
}
- 需要注意的点:
-
为Student添加了一个
private的构造函数,参数值为Builder
,建造者字段和实体字段在构造函数中一一对应赋值 -
建造者中对每个字段都添加一个方法,且返回值为建造者本身
,这样才能进行链式编程
3.1 这下能自如应对对象创建
// 创建一个23岁的小明
Student xm4 = Student.builder().name("小明").age(23).build();
// 创建一个男23岁小明
Student xm5 = Student.builder().name("小明").age(23).sex(1).build();
// 创建一个喜欢写代码的小明
Student xm6 = Student.builder().name("小明").hobby("代码").build();
// ...
3.2 新添加字段怎么办?
- 如果要新增一个
国籍的字段
,不光要在实体类中添加
,还需要在建造者中添加对应的字段
和方法
,而且还要更新实体类的构造方法
// 实体类和建造者中均新增字段
private String country;
// 建造者中添加对应方法
public Builder country(String val) {
this.country = val;
return this;
}
// 更新实体类的构造方法
private Student(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.hobby = builder.hobby;
this.sex = builder.sex;
this.describe = builder.describe;
// 新增赋值代码
this.country = builder.country;
}
完成如上工作就可以创建对象为country赋值了
Student xm7 = Student.builder().name("小明").country("中国").build();
-
那,建造者模式的
好处又有什么?
难道不是既有了JavaBeans创建对象的可读性
又避免了繁重的代码量
吗? -
题外话: 在我刚使用如上建造者模式创建对象的时候,觉得分分钟能吊打Java Beans创建对象的代码,也乐此不疲的为我要使用的实体类进行维护,但是也正所谓
“凡事都很难经得住时间的磨砺”
,当发现了更好的方法后,我变懒了!
4. Lombok的@Builder注解
4.1 注解带来的代码整洁
- 在类上注解标注@Builder注解,会自动生成建造者的代码,且和上述用法一致,而且
不需要再为新增字段特意维护代码
,也太香了吧…
@Data
@Builder
public class Student {
...
}
- 所以可以
直接标注@Builder注解
使用建造者模式创建对象(使用方法和上文中3.1节一致)
4.2 你可能听说过@Accessors要比@Builder灵活
- @Builder在创建对象时具有链式赋值的特点,
但是在创建对象后,就不能链式赋值了
,虽然toBuilder注解属性
可以返回一个新的建造者,并复用对象的成员变量值,但是这并不是在原对象上进行修改,调用完build方法后,会返回一个新的对象
// 在@Builder注解中,指定属性toBuilder = true
@Builder(toBuilder = true)
// 在创建完成对象后使用toBuilder方法获取建造者,指定新的属性值创建对象
Student xm7 = Student.builder().name("小明").country("中国").build();
Student xm8 = xm7.toBuilder().age(23).build();
- @Accessors注解可以在
原对象上进行赋值
,这里先解读一下@Accessors的源码,方便对下面的用法理解
/**
* @Accessors注解是不能单独使用的,单独标记不会产生任何作用
* 需要搭配@Data或者@Getter和@Setter使用才能生效
*/
@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface Accessors {
/**
* 这个属性默认是false,为false时,getter和setter方法会有get和set前缀
* 什么意思呢,比如字段name,在该属性为false生成的get和set方法为getName和setName
* 而当属性为true时,就没有没有get和set前缀,get方法和set方法都名为name,只不过set方法要有参数,是对name方法的重载
*/
boolean fluent() default false;
/**
* chain属性,显然从字面意思它能实现链式编程,默认属性false
* 为true时,setter方法的返回值是该对象,那么我们就能进行链式编程了
* 为false时,setter的返回值为void,就不能进行链式编程了
*
* 注意:特殊的一点是,当fluent属性为true时,该值在不指定的情况下也会为true
*/
boolean chain() default false;
/**
* 这个属性值当我们指定的时候,会将字段中已经匹配到的前缀进行'删除'后生成getter和setter方法
* 但是它也有生效条件:字段必须是驼峰式命名,且前几个小写字母与我们指定的前缀一致
*
* 举个例子:
* 我们有一个字段如下
* private String lastName
* 在我们不指定prefix时,生成的getter和setter方法为 getLastName 和 setLastName
* 当我们指定prefix为last时,那么生成的getter和setter方法 为 getName 和 setName
*/
String[] prefix() default {};
}
- 下面我们来看看用法,
它实在是很灵活
// 我们为Student类标记一个如下注解,方法不含get和set前缀,同时又支持链式编程
@Accessors(fluent = true, chain = true)
// 这里我们创建一个25岁的小明
Student xm9 = new Student().age(25).name("小明");
// do something
// 使用完之后,假设这里需要对25岁的小明的属性进行修改,可采用如下方法,之后重新复用这个对象即可
xm9.country("中国");
- 这也实在太好用了吧!
4.3 既然把@Accessors的源码读了,@Builder的源码我也讲给你听吧
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.SOURCE)
public @interface Builder {
// 指定创建建造者的方法名,默认为builder
String builderMethodName() default "builder";
// 指定创建对象的方法名,默认为build
String buildMethodName() default "build";
// 指定静态内部建造者类的名字,默认为 类名 + Builder,如StudentBuilder
String builderClassName() default "";
// 是否能重新从对象生成建造者,默认为false,上文中有使用样例
boolean toBuilder() default false;
// 建造者能够使用的范围,默认是PUBLIC
AccessLevel access() default AccessLevel.PUBLIC;
// 标注了该注解的字段必须指定默认初始化值
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface Default {
}
// 这个注解的使用是要和 @Builder(toBuilder = true) 一同使用才可生效
// 在调用toBuilder方法时,会根据被标注该注解的字段或方法对字段进行赋值
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.SOURCE)
public @interface ObtainVia {
// 指定要获取值的字段
String field() default "";
// 指定要获取值的方法
String method() default "";
// 这个值在指定method才有效,为true时获取值的方法必须为静态的,且方法参数值为本类(参考下文代码)
boolean isStatic() default false;
}
}
全网很少有人讲@ObtainVia注解,那我们就来说说
,它到底有什么用,该怎么用
- 指定field赋值
// 在类中注解标记和新增字段如下
@Builder.ObtainVia(field = "hobbies")
private String hobby;
// 供hobby获取值使用
private String hobbies = "唱跳RAP";
// 测试调用toBuilder方法,检查hobby值,若为‘唱跳RAP’证明注解生效
System.out.println(new Student().toBuilder().build().getHobby());
结果:唱跳RAP
查看编译后的源码,可以发现赋值语句hobby(this.hobbies)
,原来它是如此生效的
public Student.StudentBuilder toBuilder() {
return (new Student.StudentBuilder()).name(this.name).lastNames(this.lastNames).age(this.age)
.hobby(this.hobbies).hobbies(this.hobbies)
.sex(this.sex).describe(this.describe).country(this.country);
}
- 指定
非静态
method赋值
// 在类中标注如下注解和创建如下方法
@Builder.ObtainVia(method = "describe")
private String describe;
// 非静态方法赋值
private String describe() {
return "小明的自我介绍";
}
// 测试调用toBuilder方法,检查describe值,若为‘小明的自我介绍’证明注解生效
System.out.println(new Student().toBuilder().build().getDescribe());
结果:小明的自我介绍
查看编译后的源码,发现会调用该方法
public Student.StudentBuilder toBuilder() {
// 这里会调用该方法进行赋值,在下面生成Builder时使用
String describe = this.describe();
return (new Student.StudentBuilder()).name(this.name).lastNames(this.lastNames).age(this.age).hobby(this.hobby).sex(this.sex)
.describe(describe)
.country(this.country);
}
- 指定
静态
method赋值
// 在类中标注如下注解和创建如下静态方法
@Builder.ObtainVia(method = "describe", isStatic = true)
private String describe;
// 静态方法赋值,需要指定本类类型参数
private static String describe(Student student) {
return "小明的自我介绍";
}
// 测试调用toBuilder方法,检查describe值,若为‘小明的自我介绍’证明注解生效
System.out.println(new Student().toBuilder().build().getDescribe());
结果:小明的自我介绍
查看编译后的源码
public Student.StudentBuilder toBuilder() {
// 这里调用静态方法赋值
String describe = describe(this);
return (new Student.StudentBuilder()).name(this.name).lastNames(this.lastNames).age(this.age).hobby(this.hobby).sex(this.sex).describe(describe).country(this.country);
}
5. 番外:@Builder,@Singular 夫妻双双把家还
5.1 @Singular简介
@Singular必须搭配@Builder使用
,相辅相成,@Singular标记在集合容器字段
上,在建造者
中自动生成针对集合容器的添加单个值
、添加多个值
和清除其中值
的方法,可进行标记的集合容器类型如下(参考官方文档) java.util.Iterable
, Collection
, List
, Set
, SortedSet
, NavigableSet
, Map
, SortedMap
, NavigableMap
com.google.common.collect.ImmutableCollection
, ImmutableList
, ImmutableSet
, ImmutableSortedSet
, ImmutableMap
, ImmutableBiMap
, ImmutableSortedMap
, ImmutableTable
- 使用演示
// 在类中添加如下字段,并标注@Singular注解
@Singular
private List<String> subjects;
// 测试代码,调用单个添加和多个值添加的方法
Student xm11 = Student.builder()
.subject("Math").subject("Chinese")
.subjects(Arrays.asList("English", "History")).build();
// 查看添加结果
System.out.println(xm11.getSubjects().toString());
结果:[Math, Chinese, English, History]
// 调用clearSubjects清空方法,并查看结果
System.out.prinln(xm11.toBuilder().clearSubjects().build().getSubjects().toString());
结果:[]
5.2 @Singular源码解析
@Target({FIELD, PARAMETER})
@Retention(SOURCE)
public @interface Singular {
// 指定添加单个值的方法的方法名,不指定时会自动生成方法名,比例中为'subject'
String value() default "";
// 添加多个值是否忽略null,默认不忽略,添加null的列表时会抛出异常
// 为ture时,添加为null的列表不进行任何操作
boolean ignoreNullCollections() default false;
}
- @Singular(
ignoreNullCollections = false
)编译后的代码
public Student.StudentBuilder subjects(Collection<? extends String> subjects) {
// 添加的列表为null,抛出异常
if (subjects == null) {
throw new NullPointerException("subjects cannot be null");
} else {
if (this.subjects == null) {
this.subjects = new ArrayList();
}
this.subjects.addAll(subjects);
return this;
}
}
- @Singular(
ignoreNullCollections = true
)编译后的代码
public Student.StudentBuilder subjects(Collection<? extends String> subjects) {
// 为null时不进行任何操作
if (subjects != null) {
if (this.subjects == null) {
this.subjects = new ArrayList();
}
this.subjects.addAll(subjects);
}
return this;
}
5.3 @Singular在build方法中的细节
创建完对象后
,被标记为@Singular的列表能修改吗?我们试试
Student xm11 = Student.builder()
.subject("Math").subject("Chinese")
.subjects(Arrays.asList("English", "History")).build();
// 再添加一门Java课程
xm11.getSubjects().add("Java");
结果:抛出不支持操作的异常
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.AbstractList.add(AbstractList.java:148)
at java.util.AbstractList.add(AbstractList.java:108)
at builder.TestBuilder.main(Student.java:177)
- 为什么这样?我们看看源码中的build方法就知道了,build方法
根据不同的列表大小
走不同的初始化列表方法,返回的列表都是不能进行修改的
public Student build() {
List subjects;
switch(this.subjects == null ? 0 : this.subjects.size()) {
case 0:
// 列表大小为0时,创建一个空列表
subjects = Collections.emptyList();
break;
case 1:
// 列表大小为1时,创建一个不可修改的单元素列表
subjects = Collections.singletonList(this.subjects.get(0));
break;
default:
// 其他情况,创建一个不可修改的列表
subjects = Collections.unmodifiableList(new ArrayList(this.subjects));
}
// 下面进行忽略只看上边就好
String name$value = this.name$value;
if (!this.name$set) {
name$value = Student.$default$name();
}
return new Student(name$value, this.lastNames, this.age, this.hobby, this.sex, this.describe, this.country, subjects);
}
6. 写在最后
呼!终于写完了,做个总结吧(文末有博客对应的代码仓库)
-
@Accessors注解非常的轻便,我觉得它现在已经能cover我在业务开发中创建对象的需求了,
代码可读性高,代码量又很少
-
@Builder注解它的功能相对来说更多一些,通过方法和字段来初始化建造者的值,搭配@Singular操作列表等,但是这些功能真正的在业务开发中的应用效果,还
有待考量
巨人的肩膀
-
《Effective Java 第三版》
-
原文收录:GitHub-Enthusiasm