前言:笔者经历了人生的洗礼,好久没有更新文章了,从今天开始继续写作大业~~~
新开一个系列吧(虽然之前的系列有一些不完整),记录一下学习《Effective Java》的一些理解和实践。通过学习,再次深切的感受到读书就是跟一个巨牛逼的大神在交流。
闲话少BB,开始本文的正经内容——构建器。
1. 背景
当我们在设计一个Java
的类的时候,我们绝大多数情况下都好考虑类的构造器的实现。一般情况我们有两种方式实现:带参数的构造器和不带参数的构造器(这句话说的简直满满的技术含量,笔者才华有限,完全不会起名字)。
- 带参数的构造器,指的是构造方法的入参,基本覆盖了类中的所有的成员属性,通过调用该方法,能够实现对象的初始化。
- 不带参数的构造器,指的是一个空的构造方法,创建出一个空的对象,然后通过set方法,依次为对象的各个属性进行赋值。
2. 问题
当我们在开发的过程中,很多情况下会遇到一些这样的情况。比如我们要设计一个包含10个属性的java类,其中只有2个属性是必须初始化的,而其他8个属性,都是可选属性,并且其组合是随机的。对于这样的类,我们要设计构造方法,就会有一些难搞的地方。这种情况,我们通常使用如下两种方法来解决问题。
3. 构造函数的祖传实现
3.1 使用重叠构造器
所谓的重叠构造器,就是说先定义一个基本的构造方法,入参为该类的必选属性。接下来依次实现可选属性的构造方法,每个构造方法比前边一个,增加一个可选属性作为入参,直到最后一个构造方法包含该类的所有属性。乍听起来还有一点抽象,看一下代码实现。
public class Relative {
// 必选属性
private String selfName;
// 可选属性
private String mother;
private String father;
private String motherInLaw;
private String fatherInLaw;
private String grandfather;
private String grandmother;
private String uncle;
private String aunt;
private String brother;
private String sister;
public Relative(String selfName) {
this.selfName = selfName;
}
public Relative(String selfName, String father) {
this.selfName = selfName;
this.father = father;
}
public Relative(String selfName, String mother, String father) {
this.selfName = selfName;
this.mother = mother;
this.father = father;
}
public Relative(String selfName, String mother, String father, String grandfather) {
this.selfName = selfName;
this.mother = mother;
this.father = father;
this.grandfather = grandfather;
}
...
public Relative(String selfName, String mother, String father, String motherInLaw, String fatherInLaw,
String grandfather, String grandmother, String uncle, String aunt, String brother, String sister) {
this.selfName = selfName;
this.mother = mother;
this.father = father;
this.motherInLaw = motherInLaw;
this.fatherInLaw = fatherInLaw;
this.grandfather = grandfather;
this.grandmother = grandmother;
this.uncle = uncle;
this.aunt = aunt;
this.brother = brother;
this.sister = sister;
}
}
这样实现的后果就是,整个类文件中,构造方法的定义非常冗长,重复代码很多。在敏捷和效率为王的如今,这种方法看起来非常不牛逼啊=。=
3.2 使用JavaBeans模式创建对象
这种方式,我们只实现一个构造方法,对必选属性进行初始化。而对于其他的可选属性,在类中提供set和get方法。这样当需要为对象的某些属性进行赋值的时候,直接调用对应的set方法即可。代码如下:
public class Relative {
// 必选属性
private String selfName;
// 可选属性
private String mother;
private String father;
private String motherInLaw;
private String fatherInLaw;
private String grandfather;
private String grandmother;
private String uncle;
private String aunt;
private String brother;
private String sister;
public Relative(String selfName) {
this.selfName = selfName;
}
public String getMother() {
return mother;
}
public void setMother(String mother) {
this.mother = mother;
}
public String getFather() {
return father;
}
public void setFather(String father) {
this.father = father;
}
...
public String getSister() {
return sister;
}
public void setSister(String sister) {
this.sister = sister;
}
}
在使用的时候,通常是如下的调用:
public static void main(String[] args) {
Relative relative = new Relative("zhang san");
relative.setFather("zhang da san");
relative.setMother("liu xiao er");
relative.setGrandfather("zhang lao san");
relative.setGrandmother("li lao wu");
}
这种方式看起来舒服了不少(哪里需要点哪里),可能也是我们比较常用的实现方式。但是使用这种方式进行对象的构造,在某些场景中是存在问题的。原因是因为,从整体上看,对应的创建时分为几个步骤的,先是创建一个基本的对象,然后再分别对该对象的属性进行赋值。那么就会存在对象数据不一致的风险,可能对象还没有来得及进行set赋值,就被其他地方访问了,导致获取的属性不完整,这种问题一般情况下还不好定位。除非通过额外的手段来保证线程安全。
引用《Effective Java》的话说,就是“JavaBeans模式阻止了把类做成不可变的可能”。
4. 使用构建器进行对象构造
当祖传的两种方法都不够优(牛)雅(逼)的时候,就来介绍一下巧妙的构建器方式进行对象构造。
所谓的构建器,就是在对应的类中,声明一个构建器(静态内部类,为什么是静态内部类呢,可以思考一下哦),构建器具有与外围类相同的属性。然后实现一个类的构造方法,方法入参为构建器的对象。
当需要创建类对象的时候,先创建静态内部类的对象,即所谓的构建器。再通过类的构造方法,以构建器对象为入参,创建外围类的对象。描述的有点抽象,看一下代码:
public class Relative {
// 外围类的所有成员属性均定位为final
// 必选属性
private final String selfName;
// 可选属性
private final String mother;
private final String father;
private final String motherInLaw;
private final String fatherInLaw;
private final String grandfather;
private final String grandmother;
private final String uncle;
private final String aunt;
private final String brother;
private final String sister;
private Relative(Builder builder) {
this.selfName = builder.selfName;
this.motherInLaw = builder.motherInLaw;
this.grandmother = builder.grandmother;
this.mother = builder.mother;
this.father = builder.father;
this.sister = builder.sister;
this.grandfather = builder.grandfather;
this.fatherInLaw = builder.fatherInLaw;
this.brother = builder.brother;
this.aunt = builder.aunt;
this.uncle = builder.uncle;
}
public static final class Builder {
private final String selfName;
private String mother;
private String father;
private String motherInLaw;
private String fatherInLaw;
private String grandfather;
private String grandmother;
private String uncle;
private String aunt;
private String brother;
private String sister;
public Builder(String selfName) {
this.selfName = selfName;
}
public Builder withMother(String mother) {
this.mother = mother;
return this;
}
public Builder withFather(String father) {
this.father = father;
return this;
}
public Builder withMotherInLaw(String motherInLaw) {
this.motherInLaw = motherInLaw;
return this;
}
public Builder withFatherInLaw(String fatherInLaw) {
this.fatherInLaw = fatherInLaw;
return this;
}
...
public Relative build() {
return new Relative(this);
}
}
// 只提供属性的get方法
public String getSelfName() {
return selfName;
}
public String getMother() {
return mother;
}
...
public static void main(String[] args) {
Relative relative = new Relative.Builder("zhang san").withFather("zhang da san")
.withMother("liu da er")
.withGrandfather("zhang lao san")
.withGrandmother("li lao er")
.build();
...
}
}
通过链式调用创建Builder对象,并最后通过调用build方法,进行Relative对象的构造。
构造器的定义和使用有多种方式,但是笔者通过查阅网上资料,总结出的上述代码是一套比较好的模板,原因如下:
- 外围类的构造方法是private的,说明只能在构建器中才能调用该构造方法,客户端无法直接创建外围类的对象。
- 外围类是不可变的,因为外围类的所有属性均为final,全部是由构造方法进行赋值的。并且外围类只提供get方法,不提供set方法。
- 使用链式调用创建对象,代码更容易理解。
- 构建器的必选属性为final的,在构建器的构造方法中进行赋值。
但是构建器也不是完美的,可以看到构建器也是有一定的代码量的。所以对于小型类来说,没有必要使用构建器方式进行类对象的构造。
所以构建器的使用场景有如下两个:
- 当类中包含必选属性和可选属性
- 类的属性较多(一般超过5个)
5. 我滴妈,还有意外收获!
在进行对象构建的时候,我们还可能需要去进行参数的合法性检查。
对于使用构建器进行对象构造的时候,该如何进行参数的检查呢,无非是两种方法。废话不说,上代码
// 先创建对象,然后进行参数合法性检查
public Relative build() {
Relative relative = new Relative(this);
if ("".equals(relative.getSelfName())) {
throw new Exception("invalid param");
}
return relative;
}
// 先进行参数合法性检查,再根据构建器创建对象
public Relative build() throws Exception {
if ("".equals(this.selfName)) {
throw new Exception("invalid param");
}
return new Relative(this);
}
第一种方式是安全的,因为创建出来的relative对象中,其属性是不可变的。从创建出对象,到进行参数合法性检查,其属性的值是无法改变的。
第二种方式,存在安全风险,因为在进行构建器的合法性检查之后,到创建对象之前,构建器的对应属性是可以改变的。