Effective Java大厂实战之考虑以静态工厂方法代替构造方法

Posted 神技圈子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Effective Java大厂实战之考虑以静态工厂方法代替构造方法相关的知识,希望对你有一定的参考价值。

图片
Item1-考虑以静态工厂方法代替构造方法
Item1-考虑以静态工厂方法代替构造方法

缺点1. 只提供静态工厂方法的话,该类就无法被继承(子类化)

缺点2. API的查找比较麻烦

常见静态工厂方法的命名

from —— 类型转换方法

of —— 聚合方法

valueOf —— 比from和of更繁琐的一种替代方法

instance或者getInstance

create或者newInstance

getType

newType

Type

优点1. 有名字,代码可读性更高

优点2. 减少系统的性能开销

优点3. 可以返回所声明的返回类型的任何子类的对象

优点4. 返回对象的类可以随调用时入参的变化而变化

优点5. 当编写包含方法的类时,返回对象的类不必事先存在(解耦,隔离)

单例模式模板1——饿汉单例模式

单例模式模板2——懒汉单例模式

单例模式模板3——双锁单例模式

什么是静态工厂方法

静态工厂方法的优点

静态工厂方法的缺点

总结

什么是静态工厂方法
在Java当中,获得一个类的实例的最简单的方式就是使用new关键字,通过类的构造方法来实现对象的创建,比如这样

MyClass myClass = new MyClass();
不过在实际的开发过程当中,我们经常见到这样一种获取类对象的方式,即调用某个类的静态方法获取该类的实例,如下所示:

MyClass1 myClass1 = MyClass1.getInstance();
那么像这样的,不通过new,而是使用一个静态方法对外提供自身实例的方法,即我们所说的静态工厂方法

很多小伙伴看到这里就不禁要说了,这像极了我平时写的单例模式呀。诶,您还真说对了,我们一般就是使用静态工厂方法来实现单例模式。在这里给大家放出几个常用的单例模式实现方式,可以用来直观感受一下本节介绍的静态工厂方法

单例模式模板1——饿汉单例模式

public class Singleton
private static Singleton instance = new Singleton();

private Singleton()

public static Singleton getInstance()
    return instance;

这种写法比较简单,就是将类的构造方法设为私有,在类被加载时实例化一个类对象并交给自己的引用,由于这个类在整个声明周期内只会被加载一次,因此也只会创建一个实例,从而实现了单例模式,避免了线程同步问题

单例模式模板2——懒汉单例模式

public class Singleton
private static Singleton instance;

private Singleton()

public static Singleton getInstance()
    if (instance == null) 
        instance = new Singleton();
    
    return instance;

相比于饿汉单例模式,懒汉单例模式有意将类的实例化延迟到了这个类真正被使用的时候,这种写法起到了 Lazy Loading 的作用,但是只能在单线程的场景下使用,因为在多线程的场景下,一个线程进入了if (instance == null)的判断语句块,还没来得及往下执行的时候,另一个线程也通过了这个条件判断,此时就会产生多个实例,破坏了单例模式

单例模式模板3——双锁单例模式

public class Singleton
private volatile static Singleton instance;

private Singleton ()

public static Singleton getInstance() 
    if (instance == null) 
        synchronized (Singleton.class) 
            if (instance == null) 
                instance = new Singleton();
            
        
    
    return instance;

不难看出双锁模式就是希望解决懒汉单例模式的多线程同步问题,这里的两次判断,第一次是为了避免不必要的实例创建,第二次是为了进行同步,规避多线程问题。

这里要留意一下volatile关键字的使用,可以先思考一下,为什么我都用了 synchronized做了限制,还要加入volatile关键字呢?

首先,我们可以从volatile关键字的功能上着手,volatile关键字可以保证可见性和原子性,同时确保JVM不会对执行的指令进行重排序

其次,要知道,对象的创建不是一个原子操作,这是一个复合操作,在使用new创建一个对象实例时,可以简单将这个操作分解为如下三个动作:

为对象开辟内存空间

初始化对象

设置指向分配的内存地址的引用,并将该引用返回给调用方

其中2和3两步之间会发生指令重排序(JVM会根据处理器的特性,如CPU多级缓存,多核处理器等,适当对机器指令进行重排序,从而最大化发挥机器的性能),导致多线程时如果在初始化之前访问对象就会出现问题,为了避免多线程环境下的问题,可以使用volatile禁止指令重排序从而解决问题

静态工厂方法的优点
优点1. 有名字,代码可读性更高

随着硬件性能的提升,程序员们开始逐步从写出让机器更好看懂的代码逐步转变为写出让人类更好看懂的代码。而使用静态工厂方法,可以大大提升代码的可读性,向着代码即文档的目标更进一步。

举个例子,对于如下代码,想要获取某个特定的学院对象,使用静态工厂方法不就使得调用更加直观了嘛

public class College
private String collegeName;

// 这里我们将构造方法的访问权限设置为私有,以防止有人从外部调用它
private College(String collegeName) 
    this.collegeName = collegeName;


public static College getInformationCollege() 
    return new College("信息学院");


public static College getFinanceCollege() 
    return new College("财经学院");


public static College getEngineering() 
    return new College("工程学院");


public String getCollegeName() 
    return collegeName;

优点2. 减少系统的性能开销

这里顺便复习一下前面介绍的单例模式,这样可以减少不必要的对象创建,减少系统的开销

public class College
private String collegeName;
private static College informationCollege;
private static College financeCollege;
private static College engineeringCollege;

// 这里我们将构造方法的访问权限设置为私有,以防止有人从外部调用它
private College(String collegeName) 
    this.collegeName = collegeName;


public static College getInformationCollege() 
    // 以下是单例模式的代码
    if (College.informationCollege == null) 
        College.informationCollege = new College("信息学院");
    

    return College.informationCollege;


public static College getFinanceCollege() 
    if (College.financeCollege == null) 
        College.financeCollege = new College("财经学院");
    

    return College.financeCollege;


public static College getEngineeringCollege() 
    if (College.engineeringCollege == null) 
        College.engineeringCollege = new College("工程学院");
    

    return College.engineeringCollege;


public String getCollegeName() 
    return collegeName;

优点3. 可以返回所声明的返回类型的任何子类的对象

这里介绍一下面向对象设计的一个基本原则——【里氏替换原则】,简单来讲,就是凡是父类对象可以出现的地方,子类对象一定可以出现,而且程序察觉不出父类对象和子类对象之间的区别,如果把父类都替换成他的子类,程序依旧可以正常工作,且行为不会发生变化

举个例子:凡是“四边形类”对象可以出现的地方,“正方形类”对象一定可以出现,而且程序察觉不出“四边形类”对象和“正方形类”对象之间的区别,如果把“四边形类”都替换成“正方形类”,程序依旧可以正常工作,且行为不会发生变化

很显然,类的构造函数只能返回一个确切的类的对象,那就是它自己,而静态工厂方法则可以返回其任意子类对象,这里举个例子,大家一看便知

class Person
private Person()

public static Person getInstance() 
    return new Person();


public static Teacher getTeacher() 
    return new Teacher();


public static Programmer getProgrammer() 
    return new Programmer();
class Teacher extends Person class Programmer extends Person 

优点4. 返回对象的类可以随调用时入参的变化而变化

简单来说就是,可以通过改变入参返回不同的类对象,这样代码可以写得更加简洁优雅,并且可维护性更好,举个例子

Java当中的EnumSet这个抽象类,不能直接通过new新建,而是提供了若干静态工厂方法,可以创建EnumSet类型的对象,比如

public static <E extends Enum> EnumSet noneOf(Class elementType)
Enum<?>[] universe = getUniverse(elementType);
if (universe == null)
throw new ClassCastException(elementType + " not an enum");

if (universe.length <= 64)
    return new RegularEnumSet<>(elementType, universe);
else
    return new JumboEnumSet<>(elementType, universe);

注意细节,返回对象的类型将会随着universe数组长度的变化而变化,如果它有 64 个或更少的元素,就像大多数 enum 类型一样,静态工厂返回一个 long 类型的 RegularEnumSet 实例;如果 enum 类型有 65 个或更多的元素,工厂将返回一个由 long[] 类型的 JumboEnumSet 实例。开发者看不到这两个实现类的存在。如果 RegularEnumSet 不再为小型 enum 类型提供性能优势,它可能会在未来的版本中被消除,而不会产生不良影响。类似地,如果事实证明 EnumSet 有益于性能,未来的版本可以添加第三或第四个 EnumSet 实现。客户端既不知道也不关心从工厂返回的对象的类;它们只关心它是 EnumSet 的某个子类。

优点5. 当编写包含方法的类时,返回对象的类不必事先存在(解耦,隔离)

举个典型的例子,大家可以去看一下Java的数据库API(JDBC)的实现,里面大量使用了静态工厂方法,比如有如下这段获取数据库连接的代码

@CallerSensitivepublic static Connection getConnection(String url, java.util.Properties info) throws SQLException
return (getConnection(url, info, Reflection.getCallerClass()));
类似的服务提供者框架是这样一种系统,提供者实现了某个服务,系统将其实现公开给客户端,从而实现了客户端与实现之间的解耦。

服务提供者框架里有3个最基本的组件:

服务接口,代表某一个实现

提供者注册API,提供者通过它来注册实现

服务访问API,客户端通过它获取服务实例

客户端可以通过服务访问API来指定标准,从而选择相应的实现。如果没有指定这样的一个标准,那么API返回一个默认实现的实例,或者允许客户端循环所有可得到的实例。服务访问API是灵活的静态工厂,它构成了服务提供者框架的基础。

静态工厂方法的缺点
缺点1. 只提供静态工厂方法的话,该类就无法被继承(子类化)

理由很简单,如果父类当中没有声明为public或protected的构造方法,子类自然无法调用到它所期望的父类的构造方法,那么此时父类也就无法被子类继承

其实这个缺点再某种意义上来说可能算是因祸得福,因为它鼓励开发者使用组合而不是继承(这也是后面的条目之一,因为继承的耦合性太强,像go、rust这些语言都没有选择使用继承),并且对于不可变类而言是必需的

缺点2. API的查找比较麻烦

因为这些静态工厂方法在API文档当中不像构造方法那样引人注目,因此有时候很难弄清楚如何实例化一个只提供静态工厂方法而没有构造函数的类

常见静态工厂方法的命名

其实如果代码看的比较多的话,是可以总结出一些静态工厂方法的命名规律的,这里可以简单罗列一些我个人的总结以供大家参考

from —— 类型转换方法
只有单个参数,返回该类型的一个相对应的实例,例如

Date date = Date.from(Instant.now());
of —— 聚合方法
带有多个参数,返回该类型的一个实例,把它们合并起来,例如

Set faceCards = EnumSet.of(A, B, C);
valueOf —— 比from和of更繁琐的一种替代方法
这个就很常见了,大家平时应该用的很多

BigInteger bigInteger = BigInteger.valueOf(123L);
instance或者getInstance
返回的对象实例时,有时也通过入参对方法进行描述,例如

MessageDigest messageDigest = MessageDigest.getInstance(“MD5”);
create或者newInstance
像instance或者getInstance一样,区别在于每次调用都返回一个新的实例,例如:

Object newArray = Array.newInstance(Integer.class, 12);
getType
像getInstance一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型,例如:

FileStore fileStore = Files.getFileStore(path);FileTime fileSystem = Files.getLastModifiedTime(path);
newType
像newInstance一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型,例如:

BufferedReader bufferedReader = Files.newBufferedReader(path);
Type
getType 和 newType 的简版,例如:

List integerList = Collections.list(integerEnumeration);
总结
静态工厂方法和公共构造方法都有各自的用途,要理解其各自适用的场景。通常静态工厂的方式更可取,因此应避免在没有考虑静态工厂的情况下就直接提供公共构造方法。

本文在串讲过程当中所介绍的单例模式的代码模板、new一个对象时发生了什么、常见静态工厂方法的命名规律等,也可做为各位看官对自身已掌握知识的一次回顾。

以上是关于Effective Java大厂实战之考虑以静态工厂方法代替构造方法的主要内容,如果未能解决你的问题,请参考以下文章

Effective Java大厂实战之考虑以静态工厂方法代替构造方法

Effective Java大厂实战之考虑以静态工厂方法代替构造方法

Effective Java 学习笔记之创建和销毁对象

Effective Java读书笔记创建和销毁对象:考虑使用静态工厂方法代替构造器

读书笔记 - Effective Java01. 考虑用静态工厂方法代替构造器

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