为了账号安全,请及时绑定邮箱和手机立即绑定

《Effective Java》学习笔记(一)——构建器

标签:
Java

前言:笔者经历了人生的洗礼,好久没有更新文章了,从今天开始继续写作大业~~~
新开一个系列吧(虽然之前的系列有一些不完整),记录一下学习《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对象中,其属性是不可变的。从创建出对象,到进行参数合法性检查,其属性的值是无法改变的。
第二种方式,存在安全风险,因为在进行构建器的合法性检查之后,到创建对象之前,构建器的对应属性是可以改变的。

点击查看更多内容
2人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消