本文摘自:https://www.jianshu.com/p/ceb5ec8f1174

本文略长,所以先来个内容提要

    1. 序:什么是静态工厂方法
    1. Effective Java
    • 2.1 静态工厂方法与构造器不同的第一优势在于,它们有名字
    • 2.2 第二个优势,不用每次被调用时都创建新对象
    • 2.3 第三个优势,可以返回原返回类型的子类
    • 2.4 第四个优势,在创建带泛型的实例时,能使代码变得简洁
    1. 除此之外
      3.1 可以有多个参数相同但名称不同的工厂方法
      3.2 可以减少对外暴露的属性
      3.3 多了一层控制,方便统一修改
    1. 总结

1. 序:什么是静态工厂方法

在 Java 中,获得一个类实例最简单的方法就是使用 new 关键字,通过构造函数来实现对象的创建。
就像这样:

    Fragment fragment = new MyFragment();
// or
Date date = new Date();

不过在实际的开发中,我们经常还会见到另外一种获取类实例的方法:

    Fragment fragment = MyFragment.newIntance();
// or
Calendar calendar = Calendar.getInstance();
// or
Integer number = Integer.valueOf("3");

↑ 像这样的:不通过 new,而是用一个静态方法来对外提供自身实例的方法,即为我们所说的静态工厂方法(Static factory method)

知识点:new 究竟做了什么?

简单来说:当我们使用 new 来构造一个新的类实例时,其实是告诉了 JVM 我需要一个新的实例。JVM 就会自动在内存中开辟一片空间,然后调用构造函数来初始化成员变量,最终把引用返回给调用方。

2. Effective Java

在关于 Java 中书籍中,《Effective Java》绝对是最负盛名几本的之一,在此书中,作者总结了几十条改善 Java 程序设计的金玉良言。其中开篇第一条就是『考虑使用静态工厂方法代替构造器』,关于其原因,作者总结了 4 条(第二版),我们先来逐个看一下。

2.1 静态工厂方法与构造器不同的第一优势在于,它们有名字

由于语言的特性,Java 的构造函数都是跟类名一样的。这导致的一个问题是构造函数的名称不够灵活,经常不能准确地描述返回值,在有多个重载的构造函数时尤甚,如果参数类型、数目又比较相似的话,那更是很容易出错。

比如,如下的一段代码 :

Date date0 = new Date();
Date date1 = new Date(0L);
Date date2 = new Date("0");
Date date3 = new Date(1,2,1);
Date date4 = new Date(1,2,1,1,1);
Date date5 = new Date(1,2,1,1,1,1);

—— Date 类有很多重载函数,对于开发者来说,假如不是特别熟悉的话,恐怕是需要犹豫一下,才能找到合适的构造函数的。而对于其他的代码阅读者来说,估计更是需要查看文档,才能明白每个参数的含义了。

(当然,Date 类在目前的 Java 版本中,只保留了一个无参和一个有参的构造函数,其他的都已经标记为 @Deprecated 了)

而如果使用静态工厂方法,就可以给方法起更多有意义的名字,比如前面的 valueOfnewInstancegetInstance 等,对于代码的编写和阅读都能够更清晰。

2.2 第二个优势,不用每次被调用时都创建新对象

这个很容易理解了,有时候外部调用者只需要拿到一个实例,而不关心是否是新的实例;又或者我们想对外提供一个单例时 —— 如果使用工厂方法,就可以很容易的在内部控制,防止创建不必要的对象,减少开销。

在实际的场景中,单例的写法也大都是用静态工厂方法来实现的。

如果你想对单例有更多了解,可以看一下这里:☞《Hi,我们再来聊一聊Java的单例吧》

2.3 第三个优势,可以返回原返回类型的子类

这条不用多说,设计模式中的基本的原则之一——『里氏替换』原则,就是说子类应该能替换父类。
显然,构造方法只能返回确切的自身类型,而静态工厂方法则能够更加灵活,可以根据需要方便地返回任何它的子类型的实例。

Class Person {
public static Person getInstance(){
return new Person();
// 这里可以改为 return new Player() / Cooker()
}
}
Class Player extends Person{
}
Class Cooker extends Person{
}

比如上面这段代码,Person 类的静态工厂方法可以返回 Person 的实例,也可以根据需要返回它的子类 Player 或者 Cooker。(当然,这只是为了演示,在实际的项目中,一个类是不应该依赖于它的子类的。但如果这里的 getInstance () 方法位于其他的类中,就更具有的实际操作意义了)

2.4 第四个优势,在创建带泛型的实例时,能使代码变得简洁

这条主要是针对带泛型类的繁琐声明而说的,需要重复书写两次泛型参数:

Map<String,Date> map = new HashMap<String,Date>();

不过自从 java7 开始,这种方式已经被优化过了 —— 对于一个已知类型的变量进行赋值时,由于泛型参数是可以被推导出,所以可以在创建实例时省略掉泛型参数。

Map<String,Date> map = new HashMap<>();

所以这个问题实际上已经不存在了。

3. 除此之外

以上是《Effective Java》中总结的几条应该使用静态工厂方法代替构造器的原因,如果你看过之后仍然犹豫不决,那么我觉得可以再给你更多一些理由 —— 我个人在项目中是大量使用静态工厂方法的,从我的实际经验来世,除了上面总结的几条之外,静态工厂方法实际上还有更多的优势。

3.1 可以有多个参数相同但名称不同的工厂方法

构造函数虽然也可以有多个,但是由于函数名已经被固定,所以就要求参数必须有差异时(类型、数量或者顺序)才能够重载了。
举例来说:

class Child{
int age = 10;
int weight = 30;
public Child(int age, int weight) {
this.age = age;
this.weight = weight;
}
public Child(int age) {
this.age = age;
}
}

Child 类有 age 和 weight 两个属性,如代码所示,它已经有了两个构造函数:Child(int age, int weight) 和 Child(int age),这时候如果我们想再添加一个指定 wegiht 但不关心 age 的构造函数,一般是这样:

public Child( int weight) {
this.weight = weight;
}

↑ 但要把这个构造函数添加到 Child 类中,我们都知道是行不通的,因为 java 的函数签名是忽略参数名称的,所以 Child(int age)Child(int weight) 会冲突。

这时候,静态工厂方法就可以登场了。

class Child{
int age = 10;
int weight = 30;
public static Child newChild(int age, int weight) {
Child child = new Child();
child.weight = weight;
child.age = age;
return child;
}
public static Child newChildWithWeight(int weight) {
Child child = new Child();
child.weight = weight;
return child;
}
public static Child newChildWithAge(int age) {
Child child = new Child();
child.age = age;
return child;
}
}

其中的 newChildWithWeightnewChildWithAge,就是两个参数类型相同的的方法,但是作用不同,如此,就能够满足上面所说的类似Child(int age)Child(int weight)同时存在的需求。
(另外,这两个函数名字也是自描述的,相对于一成不变的构造函数更能表达自身的含义,这也是上面所说的第一条优势 —— 『它们有名字』)

3.2 可以减少对外暴露的属性

软件开发中有一条很重要的经验:对外暴露的属性越多,调用者就越容易出错。所以对于类的提供者,一般来说,应该努力减少对外暴露属性,从而降低调用者出错的机会。

考虑一下有如下一个 Player 类:

// Player : Version 1
class Player {
public static final int TYPE_RUNNER = 1;
public static final int TYPE_SWIMMER = 2;
public static final int TYPE_RACER = 3;
protected int type;
public Player(int type) {
this.type = type;
}
}

Player 对外提供了一个构造方法,让使用者传入一个 type 来表示类型。那么这个类期望的调用方式就是这样的:

    Player player1 = new Player(Player.TYPE_RUNNER);
Player player2 = new Player(Player.TYPE_SWEIMMER);

但是,我们知道,提供者是无法控制调用方的行为的,实际中调用方式可能是这样的:

    Player player3 = new Player(0);
Player player4 = new Player(-1);
Player player5 = new Player(10086);

提供者期望的构造函数传入的值是事先定义好的几个常量之一,但如果不是,就很容易导致程序错误

—— 要避免这种错误,使用枚举来代替常量值是常见的方法之一,当然如果不想用枚举的话,使用我们今天所说的主角静态工厂方法也是一个很好的办法。

插一句:
实际上,使用枚举也有一些缺点,比如增大了调用方的成本;如果枚举类成员增加,会导致一些需要完备覆盖所有枚举的调用场景出错等。

如果把以上需求用静态工厂方法来实现,代码大致是这样的:


// Player : Version 2
class Player {
public static final int TYPE_RUNNER = 1;
public static final int TYPE_SWIMMER = 2;
public static final int TYPE_RACER = 3;
int type; private Player(int type) {
this.type = type;
} public static Player newRunner() {
return new Player(TYPE_RUNNER);
}
public static Player newSwimmer() {
return new Player(TYPE_SWIMMER);
}
public static Player newRacer() {
return new Player(TYPE_RACER);
}
}

注意其中的构造方法被声明为了 private,这样可以防止它被外部调用,于是调用方在使用 Player 实例的时候,基本上就必须通过 newRunner、newSwimmer、newRacer 这几个静态工厂方法来创建,调用方无须知道也无须指定 type 值 —— 这样就能把 type 的赋值的范围控制住,防止前面所说的异常值的情况。

插一句:
严谨一些的话,通过反射仍能够绕过静态工厂方法直接调用构造函数,甚至直接修改一个已创建的 Player 实例的 type 值,但本文暂时不讨论这种非常规情况。

3.3 多了一层控制,方便统一修改

我们在开发中一定遇到过很多次这样的场景:在写一个界面时,服务端的数据还没准备好,这时候我们经常就需要自己在客户端编写一个测试的数据,来进行界面的测试,像这样:

    // 创建一个测试数据
User tester = new User();
tester.setName("隔壁老张");
tester.setAge(16);
tester.setDescription("我住隔壁我姓张!");
// use tester
bindUI(tester);
……

要写一连串的测试代码,如果需要测试的界面有多个,那么这一连串的代码可能还会被复制多次到项目的多个位置。

这种写法的缺点呢,首先是代码臃肿、混乱;其次是万一上线的时候漏掉了某一处,忘记修改,那就可以说是灾难了……

但是如果你像我一样,习惯了用静态工厂方法代替构造器的话,则会很自然地这么写,先在 User 中定义一个 newTestInstance 方法:

static class User{
String name ;
int age ;
String description;
public static User newTestInstance() {
User tester = new User();
tester.setName("隔壁老张");
tester.setAge(16);
tester.setDescription("我住隔壁我姓张!");
return tester;
}
}

然后调用的地方就可以这样写了:

    // 创建一个测试数据
User tester = User.newTestInstance();
// use tester
bindUI(tester);

是不是瞬间就觉得优雅了很多?!

而且不只是代码简洁优雅,由于所有测试实例的创建都是在这一个地方,所以在需要正式数据的时候,也只需把这个方法随意删除或者修改一下,所有调用者都会编译不通过,彻底杜绝了由于疏忽导致线上还有测试代码的情况。

4. 总结

总体来说,我觉得『考虑使用静态工厂方法代替构造器』这点,除了有名字、可以用子类等这些语法层面上的优势之外,更多的是在工程学上的意义,我觉得它实质上的最主要作用是:能够增大类的提供者对自己所提供的类的控制力

作为一个开发者,当我们作为调用方,使用别人提供的类时,如果要使用 new 关键字来为其创建一个类实例,如果对类不是特别熟悉,那么一定是要特别慎重的 —— new 实在是太好用了,以致于它经常被滥用,随时随地的 new 是有很大风险的,除了可能导致性能、内存方面的问题外,也经常会使得代码结构变得混乱。

而当我们在作为类的提供方时,无法控制调用者的具体行为,但是我们可以尝试使用一些方法来增大自己对类的控制力,减少调用方犯错误的机会,这也是对代码更负责的具体体现。

java静态工厂的更多相关文章

  1. Java 静态工厂模式的使用

    多相关文章请参考:http://www.enjoytoday.cn/categorys/java 静态工厂模式给人的第一印象就是:static+abstract.这两个词汇已经说明了一切,一个是周期长 ...

  2. 比较 Java 静态工厂方法与构造函数

    1 什么是静态工厂方法 Java 静态工厂方法是在方法前加上 public static,让这个方法变为公开.静态的方法.该方法返回该类的一个实例,就好像一个工厂生产出一个产品.所以称之为静态工厂方法 ...

  3. Java学习笔记(十四)——Java静态工厂

    [前面的话] 每天过的还行,对我来说,只要让自己充实,生活就会是好的. 学习Java工场方法的原因是最近在使用Spring框架做一个系统,其中有一个注入的方法是使用静态工场方法注入,所以学习一下,基础 ...

  4. [原创]java WEB学习笔记102:Spring学习---Spring Bean配置:bean配置方式(工厂方法(静态工厂方法 & 实例工厂方法)、FactoryBean) 全类名

    本博客的目的:①总结自己的学习过程,相当于学习笔记 ②将自己的经验分享给大家,相互学习,互相交流,不可商用 内容难免出现问题,欢迎指正,交流,探讨,可以留言,也可以通过以下方式联系. 本人互联网技术爱 ...

  5. 【Effective Java】1、静态工厂的方式代替构造函数

    使用一个服提供者对进行服务的提供,服务的请求通过不同的提供者提供不同的服务,服务提供者首先必须在工厂中进行注册,然后才可以通过工厂实例化服务 Service.java package cn.xf.cp ...

  6. 读Effective Java笔记之one:static Factory methods instead of Constructors (静态工厂方与构造器)

    获取类的实例的方法有很多种,在这很多种方法中,它们各有优缺,各有特点.这里,只介绍2中方法 1.使用构造方法 public class Person { private String sex; /** ...

  7. Java设计模式———静态工厂

    上课时yqj2065要求:除了JDK等框架或工具中的类,自己编写的类不得使用new创建对象(Test除外). 据说是因为使用new会涉及到硬编码.(不是很懂) 所以要求用God类利用反射+配置文件来创 ...

  8. Effective Java 第三版——1. 考虑使用静态工厂方法替代构造方法

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  9. java的设计模式 - 静态工厂方法

    静态工厂方法,也不知道为何叫这个名字.其实也就是一个静态函数,可以替代构造函数用.大名鼎鼎的 guava 就大量使用这种模式,这是非常有用的模式. 比如是 Integer i = Integer.va ...

随机推荐

  1. Visual Studio 2013 编译 64 位 Python 的 C 扩展 (使用 PyObject 包装)

    对于 32 位 Python 的 C 扩展,以前用过 mingW32 编译, 但是 mingW32 不支持 64 位 Python 的 C 扩展编译,详情可见 stackoverflow,这位前辈的大 ...

  2. 2018.11.10 FCC java分享大会

    一.<android热修复背后的java技术> 1.类是如何被jvm加载的 (1)由类加载器加载 (2)类加载器分类,基于双亲委派原则(建议) (3)关键代码 先去判断是否已经加载,如果没 ...

  3. Node.js读取文件内容并返回值(非异步)

    主要解决的问题的,以最近VsCode插件开发为例,每次请求都需要token,而vscode并不支持cookie这样的存储,所以就采用粗暴点办法,存到某个用户目录下并读取. 源码如下: var fs=r ...

  4. IDEA+'mvn' 不是内部或外部命令

    问题描述: 提示'mvn' 不是内部或外部命令,也不是可运行的程序或批处理文件. 或者提示 The JAVA_HOME environment variable is not defined corr ...

  5. # 20175329 2018-2019-2 《Java程序设计》第一周学习总结

    教材学习内容总结 ·通过教材学习了如何安装JAVA ·在安装JAVA后对其环境变量的设置 ·第一次接触并手打出JAVA语言## 教材学习中的问题和解决过程- 问题1:在环境变量设置中不懂path cl ...

  6. python:HTMLTestRunner测试报告优化

    之前的博客有介绍过python的单元测试框架unittest,基于其扩展的测试报告模块HTMLTestRunner,不过这个报告本身的界面看起来太丑... 趁着今天有时间,找了两个二次开发优化后的HT ...

  7. JavaEE学习之Spring AOP

    一.基本概念 AOP——Aspect-Oriented Programming,面向切面编程,它是spring框架的一个重要组成部分.一般的业务逻辑都有先后关系,我们可以理解为纵向关系,而AOP关注的 ...

  8. Git&Github基本操作与分支管理

    Git的原理涉及快照流.链表.指针等,这里不作过多叙述. 1.基本操作 git init 创建一个 Git 仓库 git clone [url] 拷贝一个 Git 仓库到本地 git add [fil ...

  9. go项目

    1.循环使用缓存 每条日志需要开辟缓存块来存储内容,以减少频繁的内存分配与回收.日志结构体定义如下: type MLogger struct { // freeList is a list of by ...

  10. storm自定义分组与Hbase预分区结合节省内存消耗

    Hbas预分区 在系统中向hbase中插入数据时,常常通过设置region的预分区来防止大数据量插入的热点问题,提高数据插入的效率,同时可以减少当数据猛增时由于Region split带来的资源消耗. ...