读书笔记:《Effective Java 3rd》创建和销毁对象 02.当构造方法参数过多时考虑用 builder 模式

本文探讨了Java中处理多个可选参数的三种构造模式:可伸缩构造方法、JavaBean模式和Builder模式。分析了每种模式的优缺点,并推荐了在不同场景下的适用性。


静态工厂和构造方法都有个共同的不足之处:它们不能很好地扩展到很多可选参数的情景。

当构造方法的参数较多时,有如下几种处理方式:

  • 可伸缩性的构造方法
  • JavaBean 模式
  • builder 模式

1、可伸缩性的构造方法

比如 People 类中有一个必须的属性 sex 和两个可选属性 name、address。 通常会写下这样的代码:

class People {	 
    private String sex;
    private String address;
    private String name;
 
    public People(String sex) {
        this.sex = sex;
    }
 
    public People(String sex, String address) {
        this.sex = sex;
        this.address = address;
    }
 
    public People(String sex, String address, String name) {
        this.sex = sex;
        this.address= address;
        this.name= name;
    }   
}

这种方式的弊端在于,如果可选参数很多,比如十几个,那么就得提供相应的构造方法;同时参数一多在调用的时候就容易传错参数或将参数顺序颠倒,导致运行时出错。

这种方式只适合类的成员属性较少的情况下使用(通常不超过4个最好)。

2、JavaBean模式

JavaBean规范

  1. JavaBean的类访问权限必须是 public
  2. JavaBean必须具有一个无参数的构造方法。在Java中如果没有显式声明构造方法, 会提供一个默认的构造方法, 但是如果自定义了有参数构造方法 则需要再提供一个无参的构造方法。
  3. JavaBean一般将字段设置成私有的,通过使用getter方法和setter方法来进行字段的设置。

一般字段[Field]表示的是实例的变量, 而属性[Property]表示的是getter和setter方法

Java模式案例

JavaBean模式就是先调用无参构造器创建对象,然后在调用属性的setter方法设置相应的属性值

@Setter
@Getter
@NoArgsConstructor
@ToString
class People {	 
    private String sex;
    private String address;
    private String name;
    
    public static void main(String[] args) {
        People people = new People();
        people.setAddress("中国");	
        people.setName("老王");		
        people.setSex("男");			
    }
}

优点

同可伸缩性的构造方法相比,这种方式的可读性要强,创建实例也很容易些。

缺点

1)阻止了把类做成不可变的可能性

例如假设需求是 People 对象初始化之后不能重新给 name 赋值, 最简单的方法就是将name字段声明为 final

 private final String name;

但是这样的话设置name的时候就只能通过构造方法来给name赋值了, 其setter方法编译器将会报错(Cannot assign a value to final variable ‘name’), 因为对象的 fianl 字段只能在执行构造方法时赋值。

所以在JavaBean中只能通过在setter方法中加入判断逻辑来保持不可变

public void setName(String name) {
    // 如果实例的name已经赋值过了, 就return
    if (Objects.nonNull(this.name))
        return;

    // 判断传入的name参数不为null 并且不是一个空字符串才赋值
    if (Objects.nonNull(name) && !name.isEmpty())
        this.name = name;
}

这样看起来这个字段暂时不可变的, 但是一旦需要不可变的字段变多了, 其对应 setter 方法中的判断逻辑也将增多, 而且这些代码都是重复性代码。如果是一个恶意的调用者使用反射机制来强制给这个字段赋值, 那么这段判断逻辑的代码其实是无用的。

2)多线程情况下存在线程安全问题

但是该方式有个严重的缺点。因为在构造属性是进行多次set调用进行赋值的,在多线程情况下存在线程安全问题。

假设线程A创建People实例后进行如上3个步骤;线程B在线程A执行 setName 操作后访问了变量 people,获取到的 address 为"中国",name 为"老王";线程C在线程A执行 setAddress 操作之后和 setName 之前访问了变量 people,获取到的 address 为"中国",name 为 null。

也就是说,在 People 对象的实例化过程中,线程 B 和线程 C 获取的 name 属性可能不一致。这种情况下导致的问题调试起来比较困难,程序员需要额外付出努力来确保 JavaBean方式下的线程安全问题。

3、builder模式

builder模式是23种设计模式中的一员。它不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者是静态工厂方式)得到一个 builder 对象,然后在 builder 对象上调用类似 setter 的方法来设置每个相关的可选参数。最后在调用无参的 build 方法,生成客户端所需要的对象。

常见场景中使用

@Setter
public class People {

    private String sex;
    private String address;
    private String name;

    public static class Build {
        private String sex;
        private String address;
        private String name;

        public Build sex(String sex) {
            this.sex = sex;
            return this;
        }

        public Build address(String address) {
            this.address = address;
            return this;
        }

        public Build name(String name) {
            this.name = name;
            return this;
        }

        public People build() {
            return new People(this);
        }
    }

    private People(Build build) {
        this.sex = build.sex;
        this.address = build.address;
        this.name = build.name;
    }
}

通过链式调用获得相应的对象:

    public static void main(String[] args) {
        People people = new Build().sex("男").name("张三").address("中国").build();
    }

如果使用了 lombok,可直接在类上使用 @Builder 注解来自动构建 builder 模式。

类层次结构中使用

使用平行层次的builder,每个嵌套在相应的类中。 抽象类有抽象的builder,具体的类有具体的builder。

public abstract class People {

    protected String sex;
    protected String address;
    protected String name;

    People(PeopleBuilder peopleBuilder) {
        this.sex = peopleBuilder.sex;
        this.address = peopleBuilder.address;
        this.address = peopleBuilder.address;
    }

    abstract static class PeopleBuilder<T extends PeopleBuilder, R extends People> {
        private String sex;
        private String address;
        private String name;

        public T sex(String sex) {
            this.sex = sex;
            return self();
        }

        public T address(String address) {
            this.address = address;
            return self();
        }

        public T name(String name) {
            this.name = name;
            return self();
        }

        abstract R build();

        abstract T self();
    }
}

People.PeopleBuilder 是一个带有递归类型参数的泛型类型。与抽象的 self 方法一起使得方法链在子类里能很好地运作,而且不用强转类型。Java缺乏一个 self 类型,而这种变通可谓是模仿 self 类型的惯用方法。

public class Student extends People {

    private String number;

    Student(StudentBuilder studentBuilder) {
        super(studentBuilder);
        this.number = studentBuilder.number;
    }

    static class StudentBuilder extends PeopleBuilder<StudentBuilder, Student> {

        private String number;

        public StudentBuilder number(String number) {
            this.number = number;
            return this;
        }

        @Override
        Student build() {
            return new Student(this);
        }

        @Override
        protected StudentBuilder self() {
            return this;
        }
    }
}

Student.StudentBuilder 的 build 方法返回了 Student对象。一个子类方法返回的对象类型是其父类中对应方法返回的对象类型的子类型。这种技术被称为协变返回类型。这样客户端在使用这些builder时,就不用强转了。

    public static void main(String[] args) {
        Student student = new StudentBuilder()
            .sex("女").address("中国").name("娜娜").number("No110").build();
    }

优点

  • 既具有可伸缩的构造方法的安全性(因为初始化对象的时候,构造器只会被执行一次),
    又具有 JavaBean 模式的可读性
  • 灵活

缺点

为了创建对象,首先必须创建它的 builder。虽然创建这个 builder 的成本在实践中不太可能被注意到,但在看中性能的场合下这可能就是一个问题。

4、总结

无论参数多不多,最好都在类上加上 lombok 的 @Builder 注解,因为后面如果再改成 builder模式成本就比较大了。当然,如果在特别注重性能的场合下可以考虑不使用 builder模式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值