前言
这时《Effective Java》这本书的第二节,也是我认为在实际项目中比较有用的,这一节的构建方法其实在 lombok 包下使用 @Builder注解就可以
使用构建器进行对一个类多个构造参数进行构造
1. 问题引出
其实《Effective Java》这本书在第一节的时候就提出过推荐使用静态工厂的方法去创建一个类,其中的一个好处就是不用每次都创建一个新的对象。比如单例模式中的 getInstance 方法。而如果我们抛开创建对象的好处来看,其实对于属性的提供和单纯的构造器是差不多的。内部哪个属性需要赋值就对哪个属性赋值。
1. 重叠构造器的弊端
重叠构造器 |
public class Student {
private int id;
private String name;
private String number;
private int age;
public Student() {
}
public Student(int id) {
this.id = id;
}
public Student(int id, String name) {
this.id = id;
this.name = name;
}
public Student(int id, String name, String number) {
this.id = id;
this.name = name;
this.number = number;
}
public Student(int id, String name, String number, int age) {
this.id = id;
this.name = name;
this.number = number;
this.age = age;
}
}
其实从上面这段代码中不难发现,如果我们想要对一个类里面的属性进行赋值,那么这时候的构造器的创建太麻烦了,基本每一种情况都要考虑到,上面只是列出几种情况。但是如果属性超过10个,甚至到了20个的时候,构造器的方法就使得一个类显得太臃肿了。
其实也有人想到了为什么不能弄一个全参数的构造器,每次把其他那些不需要赋值的设置为空或者为0不就可以了吗?确实,这种想法是可以的,就比如下面的代码。但是如果参数太多的时候,比如到了20个,这时候如果我们只需要对其中5个属性进行赋值,这时候我们要把剩下的15个都默认设置初始值,这种工作量实在太大了。随着参数的增加,类会变得越来越臃肿。
public Student(int id, String name, String number, int age) {
this.id = id;
this.name = name;
this.number = number;
this.age = age;
}
new Student(1, "张三", "123", 18);
总之,重叠构造器 可行,但是对于有很多参数的时候,客户端就很难写了,并且难以阅读。毕竟如果你想要知道这些参数是什么意思,那么你得一个一个参数来找才行。而且,这么多的参数,如果在赋值的时候其中两个不小心调换了,构造器有可能不报错,但是在后续的操作中,也难免会发生数据不正确的错误。
2. JavaBeans模式的弊端
JavaBeans模式 |
public class Student {
private int id;
private String name;
private String number;
private int age;
public Student() {
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
这种创建方法弥补了重叠构造器器
的不足,简单来说,就是创建对象更加容易了,而且赋值更加容易了,代码读起来也很容易,我们只需要关注 set 方法的对象是谁就行了,其他不相关的属性我们是不需要关注的。
但是,JavaBeans 模式自身有着很严重的缺点,因为构造过程分成了几步,在构造过程中,JavaBean 可能处于不一致的状态,比如多线程情况下,其实这种情况下,JavaBean 方法是很不安全的,而在这个时候使用不同的对象,就会导致产生问题。来看下面这个多线程的例子:两个线程对一个对象的属性进行赋值
public class TestJavaBeans {
static Student student = new Student();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
try {
System.out.println("t1线程设置了name为张三");
student.setName("张三");
Thread.sleep(2000);
System.out.println("线程t1要进行它的方法");
if(!student.getName().equals("张三")){
System.out.println(Thread.currentThread().getName() + ": 我的张三怎么被修改了???");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
Thread t2 = new Thread(()->{
try {
Thread.sleep(1000);
student.setName("李四");
System.out.println("t2线程设置了name为李四");
System.out.println("线程t2要进行它的方法");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
}
}
执行结果:
t1线程设置了name为张三
t2线程设置了name为李四
线程t2要进行它的方法
线程t1要进行它的方法
t1: 我的张三怎么被修改了???
上面这个例子是为了说明多线程情况下不安全的情况,线程 t1 设置的属性 张三 被线程 t2 修改为了李四。当然看了上面的例子你可能会觉得有谁在正常项目中会这么写。但其实多线程下往往一些细节问题都可能导致一些代码的错误
比如上面的代码 set 方法没有设置线程安全的情况,这时侯线程t1和线程t2对这个类同时赋值,其实上面这样设计本意是线程 t1 先赋值,然后用这个对象去进行 t1
的方法,而这时候由于睡眠了一段时间,导致线程 t2 把这个属性改了,原来预先设定的执行顺序是:
线程t1设置张三 ---> 线程t1执行方法 --> 线程t2设置李四 ---> 线程t2执行方法
后来顺序变成了:
线程t1设置张三 --> 线程t2设置李四 ---> 线程t2执行方法 ---> 线程t1执行方法
实际项目中其实我们基本很少加睡眠,但是这里的睡眠可以理解为 CPU时间片
,此时 t1 线程分配的 CPU 时间片刚好用完,那么此时 t1 线程的赋值就会被其他线程篡改。
除了上面说的这些影响,还有一点不足就是 JavaBeans 模式使得类做成不可变的可能性不复存在。其实很好理解 set 方法本来就是赋值的,要解决也可以,这就需要程序员手动冻结了,调用 freeze 方法来进行冻结,但是这种方法有可能会导致运行时发生错误,因为编译器无法确保程序员会在使用之前把这个对象冻结(冻结就是这个类属性不可变了)。
2. 解决
建造者模式 |
public class Student {
private final int id;
private final String name;
private final String number;
private final int age;
public static class Builder{
private int id = 0;
private String name = null;
private String number = null;
private int age = 0;
public Builder id(int val){
id = val;
return this;
}
public Builder name(String val){
name = val;
return this;
}
public Builder number(String val){
number = val;
return this;
}
public Builder age(int val){
age = val;
return this;
}
public Student build(){
return new Student(this);
}
}
public Student(Builder builder) {
id = builder.id;
name = builder.name;
number = builder.number;
age = builder.age;
}
}
测试结果:
可以看到,返回的学生类是唯一的,并且这个对象里面没有 set 方法,是不可变的。
当然了,对于类层次结构,Builder 也是合适的。使用平行的 Builder,每个类都有各自的 Builder,抽象类有抽象类的,具体实现类有具体实现类的。这里用 effective java 中的例子,使用类层次根部的一个抽象类表示各种各样的比萨:
public abstract class Pizza {
public enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGU}
final Set<Topping> toppings;
abstract static class Builder<T extends Builder<T>>{
//用于创建具有指定元素类型的空EnumSet。
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
//添加料,并返回self()
public T addTopping(Topping topping){
toppings.add(Objects.requireNonNull(topping));
return self();
}
abstract Pizza build();
//子类实现这个方法并且返回 this
protected abstract T self();
}
Pizza(Builder<?> builder){
toppings = builder.toppings.clone(); //克隆
}
}
//经典纽约风味
public class MyPizza extends Pizza{
public enum Size{SMALL, MEDIUM, LARGE}
private final Size size;
public static class Builder extends Pizza.Builder<Builder>{
private final Size size;
public Builder(Size size) {
this.size = Objects.requireNonNull(size);
}
@Override
public MyPizza build() {
return new MyPizza(this);
}
@Override
protected Builder self() {
return this;
}
}
MyPizza(Builder builder) {
super(builder);
size = builder.size;
}
}
public class Calzone extends Pizza{
private final boolean sauceInside;
public static class Builder extends Pizza.Builder<Builder>{
private boolean sauceInside = false;
public Builder sauceInside() {
sauceInside = true;
return this;
}
@Override
public Calzone build() {
return new Calzone(this);
}
@Override
protected Builder self() {
return this;
}
}
private Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
}
测试:
3. 总结
直接引用 eff java 中的:和简单构造器对比,builder 的优势是它可以由多个可变参数。因为 builder 是利用单独得方法来设置每一个参数。其次,构造器还可以将多次调用某一个方法传入的参数传入同一个域中,比如前面的 addToppins 方法,传入的消息在 toppings里面。
使用 Builder 模式也比较灵活,因为可以利用单个 builder 创建多个不可变的多谢。builder 的参数可以根据不同的对象进行不同的调整,此外使用 builder 还可以自动填充数据。比如在对一个随机 id 赋值的时候,可以在构造器中就调用生成随机数的方法。最后,也是重要的一点,builder 模式生成的是不可变的对象,不用担心会被篡改。
当然,Builder 模式也有不足,首先就是 Builder 模式是使用 builder 来创建对象的,这样必然会带来内存的损耗,但是其实也算是不明显的。可是如果在一些十分注重性能的项目中,使用 builder 就可能出问题了。其次,builder 模式比重叠模式还冗长,毕竟里面声明了 builder,再通过 builder 给原来类的属性进行赋值,这样的代码流程就有点长了。而且其实如果像上面 builder 对类的使用来写,是比较难读懂的,除非你很熟练这种模式。所以一般在参数少的情况下才使用,而一般参数多的情况,就考虑用 builder。当然,对于 builder 模式,一开始最好就觉得要不要用,否则等项目成型了,发现参数过多的时候,使用 builder 之后要修改的代码也很多,比如我写过的一个项目,随便一个类的属性最少都有6、7个,其实这时候使用 builder 就是不错的选择。
总之,使用 builder 对于那些属性多的类来说是不错的选择,而使用这种模式创建对象的过程也很容易读懂,前提是你要懂得建造者模式
如有错误,欢迎指出!!!