引言
Java语言中的一部经典著作《Effective Java》,里面涵盖了78条我们应该熟练的Java编程技巧。
本篇博客是该书学习的系列笔记第一篇。本系列博客不会与书中的78条建议完全匹配。只是以一种读者的身份来记录和总结从书中得到的好的编程建议,博客中会明确从书中哪条建议得来的知识。
本博客总结自书中第二条:遇到多个构造器参数时要考虑用构建器
构建器的产生
我们知道对象的构建有很多种方式:构造器、静态工厂、Setter。
通常,最傻瓜式的对象构建方式是:通过new关键字,调用一个默认的无参构造,然后向这个对象中set各种属性。
第二种稍微上点档次的方式是使用构造器。为一个JavaBean创建一个有参构造,在new的时候通过有参构造来构建这个bean。
第三种是静态工厂,简单的说也是通过一个静态方法,来调用一个有参构造,来构建对象。
这三种方法貌似都没什么问题。但是设想一下这样的场景:
一个JavaBean中有很多属性,大于5个,这些属性有些是必需的,有些是可选的,如何来选择构建对象的方式?
上述三种方式:有参构造、静态工厂、Setter属性,依然可以达到场景的要求。但是它们在面对大量可选参数的情况下并不能很好的扩展。原因如下:
有参构造(或静态工厂):需要提供针对可选参数的多个不同参数列表的构造器,从而满足不同场景下对可选参数的赋值,不同构造器之间参数列表的差别很细微,可读性非常差,这种方式叫“重叠构造器”。
Setter属性:这种方式虽然比较灵活,但是有可能有线程安全问题,因为在调用构造器与set方法之间,对象并未完全构建完毕,这个时候如果在多线程的环境下有可能使未完成构建的对象处于一种不安全的状态。这种方式叫“JavaBeans模式”。
总之一句话:重叠构造器(或重叠静态工厂)可以构造出线程安全的JavaBean,但是可读性差,扩展性不强;而JavaBeans模式,虽然可读性好,灵活,但是不具备线程安全性,可能发生线程安全问题。
这就产生了新的构造对象的方式:构建器。它是“建造者模式”的一种形式,同时具备灵活性、线程安全性、可读性等多种优点,但是可能也要面临额外的编码量、理解难度、可能存在的性能开销等问题。一般在某个对象含有大量可选参数的情况下使用构建器。
构建器的使用
说了这么多,到底构建器是什么?我们如何使用构建器呢?
首先需要明确一点,构建器是用于构建一个对象的。它兼顾了构造器的优点和setter的优点。
客户端使用一个构建器需要完成三件事:
1、不直接生成目标对象,而是让客户端代码利用所有必需参数调用构造器(或静态工厂)创建builder对象(builder对象用于后续目标对象的创建和赋值工作)。
2、客户端在builder对象上调用类似Setter方法,来设置每个可选参数。
3、客户端调用无参的builder()方法生成目标对象。
因此,客户端代码可以像下面这样:
// 客户端代码使用必需参数创建一个builder对象
Student stu = new Student.StudentBuilder(10001, "张三", 1, 17)// 必须参数:学号、姓名、性别、年龄.headTeacherName("张老师") // 可选参数:班主任名称.className("三年一班") // 可选参数:班级名称.address("朝阳区-潘家园") // 可选参数:家庭住址.specialty("硬笔书法") // 可选参数:特长.build();
可以看到上面的代码中,一条语句就完成了对学生对象的创建,没有使用setter方法将可选参数的设置与构造器分开,因此不会存在线程安全问题,可读性也非常强,可选参数可以根据不同的场景自由剪裁,因此扩展性也非常高。
构建器的创建
和构造器和Setter方法一样,我们同样需要在定义JavaBean对象的类中定义一个构建器,让它来完成对目标对象的构建工作。
同样,构建器的编码工作我们需要注意几个关键步骤:
1、构建器实际上是一个静态成员类
2、构建器类中拥有所有目标对象的属性
3、构建器类拥有设置必需参数的构造器和设置每个可选参数的“类似Setter方法”
4、“类Setter方法”返回构建器对象本身
5、构建器有一个无参的build()方法,用于创建目标对象
6、目标类有唯一的私有构造器,构造器参数就是构建器对象,这个构造器的工作就是将参数中的每个属性一 一赋值给目标对象。
和前面客户端创建Student对象对应的Student类代码如下:
public class Student {// 必需参数private int stuNo;private String name;private int gender;// 性别:0代表女生;1代表男生private int age; // // 可选参数private String headTeacherName;// 班主任名称private String address;// 家庭住址private String className;//班级名称private String specialty;//特长private Student(StudentBuilder builder) {this.stuNo = builder.stuNo;this.name = builder.name;this.gender = builder.gender;this.age = builder.age;this.headTeacherName = builder.headTeacherName;this.address = builder.address;this.className = builder.className;this.specialty = builder.specialty;}/** 用于构建Student对象的构建器类*/public static class StudentBuilder {// 必需private int stuNo;private String name;private int gender;private int age;// 可选private String headTeacherName;// 班主任名称private String address;// 家庭住址private String className;//班级名称private String specialty;//特长public StudentBuilder(int stuNo, String name, int gender, int age) {this.stuNo = stuNo;this.name = name;this.gender = gender;this.age = age;}public StudentBuilder headTeacherName(String headTeacherName) {this.headTeacherName = headTeacherName;return this;}public StudentBuilder address(String address) {this.address = address;return this;}public StudentBuilder className(String className) {this.className = className;return this;}public StudentBuilder specialty(String specialty) {this.specialty = specialty;return this;}public Student build() {return new Student(this);}}
}
建议给Student添加一个toString()方法,并执行一下客户端代码:通过构建器创建Student对象并输出它。
可以看到,使用构建器的方式确实增加了一部分编码量,因此,并不是以后所有的情况都是用构建器模式才算好的代码。这种构建器模式通常用于参数较多的情况下。
总结
构建器可以解决因为大量可选参数使用重叠构造器导致客户端代码编码困难、代码可读性差、扩展性不强等缺点;同时具备类似Setter的灵活度和可读性。
同时,剖析了构建器实现的本质后,我们是否也可以简化这种构建器实现方式呢?当然可以,一个简单的JavaBean,让它的有参构造传入所有必需的参数,而它的Setter方法将void改为返回当前JavaBean对象不就完全可以达到这种串行构建对象的效果吗?书中构建器后半部分的阐述中提到了强加验证的思想,但并没有提供代码的实现思路和案例,因此在真正使用构建器的时候我们依然可以继续深入的研究构建器的使用方式,真正的掌握构建器的使用。
以上就是构建器的学习和使用,欢迎大家文末留言讨论。