当一个类中,有一些必填属性,有一些可选属性(比如人,有必填域姓名,有选填域年龄、性别、国家等),可以采用什么方法来编写呢?本文参考《Effective Java》中 “遇到多个构造器参数时要考虑使用构建器” 给出几种方法。
1. 重叠构造器
程序员习惯使用重叠构造器(telescoping constructor)模式,在这种模式下,提供的第一个构造器只有必要的参数,第二个构造器有一个可选参数,第三个构造器有两个可选参数,依次类推,最后一个构造器包含所有可选的参数。下面有个示例。
package com.test.model;
/**
* @description 重叠构造器
* @date 2020/4/23 10:51
*/
public class Person {
private String name; // 必填属性
private String age; // 选填属性
private String sex; // 选填属性
private String country; // 选填属性
public Person(String name) {
this(name, null);
}
public Person(String name, String age) {
this(name, age, null);
}
public Person(String name, String age, String sex) {
this(name, age, sex, null);
}
public Person(String name, String age, String sex, String country) {
this.name = name;
this.age = age;
this.sex = sex;
this.country = country;
}
}
当创建实例时,可以根据实际参数情况,选择使用具体的构造器,如下所示。
Person person = new Person("张三");
Person person1 = new Person("李四", "12");
当只有一个参数、两个参数、三个参数等少量参数的时候,使用构造器传参会相对比较明确,重叠构造器模式可行。但是当有许多参数的时候,客户端代码会很难编写,并且难以阅读。对于多参的构造器,会涉及到参数顺序的问题,参数太多,难以阅读。如果客户端不小心颠倒了其中两个参数的顺序,编译器也不会出错,但是程序在运行时会出现错误的行为。
2. JavaBeans模式
遇到许多可选的构造器参数的时候,还有第二种替代方法,即 JavaBeans 模式,在这种模式下,先调用一个无参构造器来创建对象,然后再调用 setter 方法来设置每个必要的参数,以及每个相关的可选参数。
package com.test.model;
/**
* @description JavaBeans 模式
* @date 2020/4/23 11:24
*/
public class Person1 {
private String name; // 必填属性
private String age; // 选填属性
private String sex; // 选填属性
private String country; // 选填属性
public Person1() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
}
创建实例,如下所示。
Person1 person1 = new Person1();
person1.setAge("12");
这种模式弥补了重叠构造器模式的不足,创建实例很容易,代码可读性强。然而,Java Beans 模式自身有很严重的缺点。因为构造过程被分到了几个调用中,在构造过程中,Java Beans 可能处于不一致的状态。类无法仅仅通过检验构造器参数的有效性来保持一致性。试图使用处于不一致状态的对象将会导致失败,这种失败与包含错误的代码大相径庭,因此调试起来十分困难。与此相关的另一点不足在于,Java Beans 模式使得把类做为不可变的可能性不复存在,这就需要程序员付出额外的努力来确保它的线程安全。
在使用 Java Bean 的时候,尤其是多线程的时候,获取同一个 Java Bean(可以通过序列/反序列获取),那么下面模拟一个场景。
线程A: 获取person,对其name age sex 就行 set 操作
同样线程B: 获取person,对其进行 get 操作
这时候会出现一种情况,在线程A中没有 set 完毕,线程 B 就开始取相应的属性。
那么就会造成 javabean 处于不一致的状态,与之相关的就是线程安全的问题,所以 javabean 作为数据的一个填充,要进行必要的保护性拷贝。
3. 建造者(Builder) 模式
建造者模式既能保证像重叠构造器模式那样的安全性,也能保证像 Java Beans 模式那么好的可读性。下面是一个建造者模式的示例。
package com.test.model;
public class Person2 {
private String name; // 必填属性
private String age; // 选填属性
private String sex; // 选填属性
private String country; // 选填属性
public Person2(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.sex = builder.sex;
this.country = builder.country;
}
public static class Builder {
private String name;
private String age;
private String sex;
private String country;
public Builder(String name) {
this.name = name;
}
public Builder setName(String name) {
this.name = name;
return this;
}
public Builder setAge(String age) {
this.age = age;
return this;
}
public Builder setSex(String sex) {
this.sex = sex;
return this;
}
public Builder setCountry(String country) {
this.country = country;
return this;
}
public Person2 build() {
return new Person2(this);
}
}
}
它不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器,得到一个 builder 对象。然后客户端在 builder 对象上调用类似于 setter 的方法,来设置每个相关的可选参数。最后,客户端调用无参的 build 方法来生成通常是不可变的对象。这个 builder 通常是它构建的静态成员类。
生成对象的方法如下所示。
Person2 person2 = new Person2.Builder("张三").setSex("男").setCountry("中国").build();
Person2 person3 = new Person2.Builder("李四").setCountry("中国").build();
Builder 模式模拟了具名的可选参数。与构造器相比,builder 的略微优势在于,它可以有多个可变参数,因为 builder 是利用单独的方法来设置每一个参数。
Builder 模式也有它自身的不足。为了创建对象,必须先创建它的构建器。虽然创建这个构建器的开销在实践中可能不那么明显,但是在某些十分注重性能的情况下,可能就成问题了。Builder 模式还比重叠构造器模式更加冗长,因此它只有在很多参数的时候才使用,比如4个或更多参数。如果已知将来就可能需要添加更多的参数,通常一开始就使用构建器。
简而言之,如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder 模式就是一种不错的选择,特别是当大多数参数都是可选或者类型相同的时候。与使用重叠构造器模式相比,使用 Builder 模式的客户端代码将更易于阅读和编写,构建器也比 Java Beans 更加安全。