第一条建议:考虑用静态工厂方法代替构造方法——《Effective Java》读书笔记

目录
一、前言
二、第一条建议
三、解释含义
四、使用 “静态工厂方法” 有什么好处
五、使用 “静态工厂方法” 有什么弊端
六、写在最后

一、前言

《Effective Java》读书笔记 系列文章用于整理和记录《Effective Java》一书的笔记,一来方便日后查阅和复习,二来与大伙共同学习与分享,并非商用,请大家尊重知识产权?。

小盆友并未一成不变的将书本的观点 cv 到这篇博文中,而是加上自己的一些 “不太成熟” 的见解,将书中的观点转为自己所认知的理解来进行记录。话不多说,开始今天的分享。

二、第一条建议

考虑用静态工厂方法代替构造方法

三、解释含义

对于创建一个类的实例,一般我们都会考虑通过该类的 公有构造函数 创建一个实例,这是我们平常最为常见的做法。但是我们或许还可以考虑另外一种方法,就是 静态工厂方法 来创建我们所需的对象。

举个例子,我们创建一个 Boolean 对象,以两种方式创建,则如下代码

// 第一种:公有方法创建
Boolean b1 = new Boolean(true);

// 第二种:静态工厂方法创建
Boolean b2 = Boolean.valueOf(true);

四、使用 “静态工厂方法” 有什么好处

(1)静态工厂方法有属于自己的方法名称,而构造函数的名称永远只是类的名称,即使有n个构造函数也是一样的名称。

在大学时听到老师们说的最多一句话是 “代码是先给人看,在给机器运行”,对于机器来说,只要逻辑是正确的,属性名、函数名对他们来说都是无意义的,而 可读的代码 对维护和迭代来说确实至关重要的。

举个例子,如下两段代码,其实功能是一样的,而在没有注释的情况下(程序员都不喜欢写注释?)或是迅速查阅 API 时,构造函数此时的 描述能力 明显弱于静态工厂方法,通过静态工厂方法的名称我们知道这个函数是可能返回素数,而且还方便我们记忆,毕竟记一串参数类型和顺序是很难的,而一个有意义的名称就很容易了。

// 构造函数
public BigInteger(int bitLength, int certainty, Random random) 

// 静态工厂方法
public static BigInteger probablePrime(int bitLength, Random random) {
    return new BigInteger(bitLength, 100, random);
}

“静态工厂方法” 优势小节:
1、代码可读性强,便于迅速查找和定位函数;
2、方便记忆;

(2)可以避免每次都创建一个新的对象

这一好处,其实我们很多人都在使用着,因为它提供了我们编程中一直提到的一个词 “复用”,而复用带来的直接好处内存的节省和性能的提升(还有其他的连锁好处)。

而且,我们可以控制该类在系统中存在的实例个数,而带来的好处是我们可以直接使用 == 代替 equal 来判断两个实例是否是相等,达到性能的提升

举个例子,如下代码,我们最开始使用的 Boolean 的例子中就已经体现了,静态工厂方法 valueOf 每次都是返回同一对象,而对于这种 不可变内容 的类来说,更为节省空间。而通过 公有构造方法 创建,每次都是新的实例,所以只能通过 equals 来比较。

// 公有构造方法创建
Boolean b1 = new Boolean(true);
Boolean b2 = new Boolean(true);

// 静态工厂方法创建
Boolean b3 = Boolean.valueOf(true);

// 静态工厂方法创建
Boolean b4 = Boolean.valueOf(true);

// 输出值
System.out.println("b1 = " + b1);
System.out.println("b2 = " + b2);
System.out.println("b3 = " + b3);
System.out.println("b4 = " + b4);

// 比较对象(注意这里不是比较值,而是对象)
System.out.println("b1 == b2 --->" + (b1 == b2));
System.out.println("b1 == b3 --->" + (b1 == b3));
System.out.println("b3 == b4 --->" + (b3 == b4));

// 比较值
System.out.println("b1.equals(b2) --->" + (b3.equals(b4)));
System.out.println("b1.equals(b3) --->" + (b1.equals(b3)));
System.out.println("b3.equals(b4) --->" + (b3.equals(b4)));

运行结果:
第一条建议:考虑用静态工厂方法代替构造方法——《Effective Java》读书笔记_第1张图片

(3)返回任何其子类型的对象

看到书中提及的这个好处,蹦到我脑海中的是两个词:“多态”“面向接口编程”。我们借助下面这张小盆友灵魂手绘的图来讲解:

第一条建议:考虑用静态工厂方法代替构造方法——《Effective Java》读书笔记_第2张图片

各个部分说明:

  • Tree 是一个接口(个人觉得某些情况下可以是抽象类);
  • A、B、C 为 Tree 的具体实现类;
  • Factory 为我们所说的 静态工厂方法 所存在的类;
  • Client 则为 API 的调用者;

“面向接口编程” 在这里的好处是,屏蔽了具体的实现类(此处的A、B、C)。如果将 A、B、C 三个具体实现类的可访问的作用域限制为 非公有的,用户获得的只能是一个 Tree 类型 的接口,则可以达到剔除对用户来说无用的方法的作用,最终达到 API简洁 的效果。毕竟用户只关心他们能使用的,过多的暴露没用的方法,只会增加代码的维护成本和降低代码的可读性。(PS:java.util.Collections 类便是这么操作的)

用户通过静态工厂方法获取所需的实例,而不知具体实现的另一好处是,在API版本升级,或是我们开源项目升级中,替换背后真正的实现类的成本近乎为0。以图中为例,API为1.0时,Factory 返回的为 C(小树苗)类,到了API为2.0时,Factory返回的为 B(小树)类,到API为2.2时,Factory返回的为A(参天大树)类。而这一切的替换对用户已经写好的代码或是将来要写的代码都是不会产生影响的

用户通过静态工厂方法获取所需的实例,还有第三个好处,便是我们可以进行配置。这个场景的最好解释就是众所周知的JDBC。各大数据库厂商只需实现对应的接口规则,然后注入服务后,用户便可进行操作。 我们还是继续以图来举例,假设现在来了 D(圣诞树)类,只需要实现 Tree 接口,然后到 Fractory 进行“登记”(具体我们可以使用Map之类的集合进行持有),最后等到用户需要时,将其作为返回值返回,但对于用户来说,D还是一个Tree类型,没什么特殊差异的存在。

五、使用 “静态工厂方法” 有什么弊端

凡事有利便有弊,认清弊端才能更好的使用这一建议。

(1)类如果不含共有的或者受保护的构造器,就不能被继承

不含共有的或者受保护的构造器,用户是无法进行继承扩展的,例如上面手绘图中,A、B、C三个类不开放构造器,用户也就是没法继承了。但也带来一个好处,他鼓励我们使用复合,而非继承(复合优于继承)。

(2)他们和其他的静态方法实际上没有任何区别

话说回来,其实 静态工厂方法 和 静态方法 其实是对编译器来说是完全一样的,都是静态方法。所以对调用 API 的用户来说,需要有一个惯用名称的协定,让他们知道这是一个非普通的静态方法。这里罗列几个,以便大家参考:

  • valueOf 类型转换方法
  • ofvalueOf,只是一个缩写
  • getInstance 返回唯一实例
  • newInstance 返回新的实例

六、写在最后

文章融入一些个人的见解,可能存在一些认知的误区或是表述不为清晰的地方,请于评论区留言我们进行讨论。如果文章对你的开发思维有些许的启发,请给我一个赞❤️或是关注我吧,技术之路路漫漫,让我们共同成长。

你可能感兴趣的:(Effective,Java,《Effective,Java》读书笔记)