传统的方式获取一个类的实例,是通过提供一个 public 构造器。这里有技巧,每一个程序员应该记住。一个类可以对外提供一个 public静态工厂方法 ,该方法只是一个朴素的静态方法,不需要有太多复杂的逻辑,只需要返回该类的实例。

这里通过 Boolean (是原始类型 boolean 的包装类)举一个简单的例子:

    public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}

这个方法,将一个 boolean 原始类型的值转换为 Boolean 对象引用。

值得注意的是,本条目中说的一个 静态工厂方法 不同于 设计模式 的工厂模式[1],同样的,本条目中描述的静态工厂方法在设计模式找不到对应的模式。

一个类可以对外提供静态工厂方法,来取代 public 的构造器,或者与 public 构造器并存,对外提供两种方式获取实例。用静态工厂方法取代 public 构造器,既有优势也有缺点。

优势体现在下面几点:

  1. 静态工厂方法与构造器比起来,它可以随意命名,而非固定的与类的名字保持一致。

    如果一个构造器的参数本身,不能对将要返回的对象具有准确的描述。此时使用一个具有准确描述名字的静态工厂方法是一个不错的选择。它可以通过名字对将要返回的对象,进行准确的描述。使得使用的人可以见名知意。

    举个例子, BigInteger(int, int, Random) 构造器,返回的值可能是素数,因此,这里其实可以有更好的表达,通过使用一个静态工厂方法 BigInteger.probablePrime(int, int, Random) 该方法于 1.4 被加入。

    一个类只能有一个指定方法签名[2]的构造器。通常我们都知道如何绕过这限制,通过交换参数列表的顺序,得到不同的方法签名。但是这是一个很糟糕的主意。这给使用 api 的开发人员造成负担,他们将很难记住哪一个方法签名对应哪一个对象的返回,最后往往都是错误的调用。阅读代码的人同样也蒙圈,如果没有相应的文档告诉他们,不同的方法签名对应的构造器返回的对象是什么。

    静态工厂方法不受上述限制,不需要去通过交换参数顺序来彼此区分,因为它们可以拥有自己的名字。因此,当一个类的多个构造器,方法签名差不多,仅仅参数顺序不一样的时候,考虑使用静态工厂方法,仔细的为静态工厂方法取名字,以区分它们之间的不同。

  2. 静态工厂方法与构造器比起来,不必每次调用都创建新的对象

    这允许不可变的类使用预创建的实例[3],或者在创建实例的时候,将实例缓存起来[4],重复的使用该实例,避免创建不重要的重复对象。Boolean.valueOf(boolean) 方法使用该技巧,它永远都不会创建对象,返回的都是预创建好的对象。这个技巧有点类似于设计模式中的享元模式[5] 。它能大幅度的提高性能,特别是在特定场景下:一些对象创建的时候,需要花费很大性能,并且这些对象经常被使用。

    该特性允许类在任何时候,对其产生多少实例具有精确的控制。用这种技巧的类,被称为实例受控的类。这里有几个使用实例受控类的理由。实例受控允许一个类保证它是一个单例或者不可实例化的类。同样的,实例受控,也可以保证不可变类不会存在两个相等的实例。

  3. 静态工厂方法与构造器比起来,可以返回该类的任意子类型的对象

    具有足够的灵活性,在获取对象的时候。可以返回协变类型,在方法中使用该类的子类构造器创建对象,然后返回,同时对外不需要暴露这些子类对象,适合于面向接口编程。在 1.8 之前,接口中不能有静态方法,针对情况的惯例做法是,针对名为 Type 类型的接口,它的静态方法被放在一个不可实例化的类 Types[6],典型的例子是 java.util.Collections 类,它通过静态工厂方法,可以构建返回各式各样的集合:同步集合、不可修改的集合等等,但是返回的时候都是返回接口类型,具体实现类型不对外公开。

    // Collections 中非公开类,同步map
    private static class SynchronizedMap<K,V>
    implements Map<K,V>, Serializable {
    private static final long serialVersionUID = 1978198479659022715L; private final Map<K,V> m; // Backing Map
    final Object mutex; // Object on which to synchronize SynchronizedMap(Map<K,V> m) {
    this.m = Objects.requireNonNull(m);
    mutex = this;
    } SynchronizedMap(Map<K,V> m, Object mutex) {
    this.m = m;
    this.mutex = mutex;
    } // Collections 的静态工厂方法,返回接口接口map,但是内部是返回同步Map类型。
    public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
    return new SynchronizedMap<>(m);
    }

    起到简化 api 的作用,这种简化不仅仅是 api 体积上的减少,减少对外暴露的类的数量,也给程序员直观上的简洁,他们只需要记住接口类型即可,无需记住具体的实现类型。这样也督促程序员面向接口编程,这是一种好的习惯。

    1.8 以后,接口可以写静态方法。因此,不再需要按照以前的习惯,为接口,写一个对应的类,直接在接口中写静态工厂方法。但是关于返回的实现类型,依然应该继续隐藏,使用非公开类实现。

  4. 静态工厂方法与构造器比起来,可以随着传入参数的不同,返回不同的对象。

    可以返回任何子类型,和第三条一样,但是可以继续添加控制,根据传入参数的不同,返回不同的对象。

    一个例子,EnumSet ,是一个不可实例的类,只有一个静态工厂方法,没有构造器。但是在 openJDK 的实现中,具体的返回类型,是根据实际枚举的个数决定的,如果小于等于 64,则返回 RegularEnumSet 类型,否则返回 JumboEnumSet 类型。这两个类型对于使用者来说,都是不可见的。如果 RegularEnumSet 类型,在将来不再为小的枚举类型提供优势,即便在未来的发行版删除 RegularEnumSet 也不会有什么影响。同样的,未来也可以继续添加第三个、第四个版本,对使用者也是无感的。使用者不需要关心具体返回的是什么对象,他们只知道,返回的对象都是 EnumSet 类型。

    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
    Enum<?>[] universe = getUniverse(elementType);
    if (universe == null)
    throw new ClassCastException(elementType + " not an enum"); // 如果小于等于 64 ,则返回 RegularEnumSet
    if (universe.length <= 64)
    return new RegularEnumSet<>(elementType, universe);
    else
    return new JumboEnumSet<>(elementType, universe);
    } // RegularEnumSet 类是 EnumSet的子类
    class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> { ...
    }
  5. 静态工厂方法与构造器比起来,在编写静态工厂方法的时候,具体的类型可以不存在。

    这句话还是面向接口的优势,意思就是,我们在编写方法的时候,可以没有任何实现类,在使用的使用,先注册实现类,然后再返回实现类,这使得扩展变得很容易。我们只是在维护一个框架,一个接口,具体的实现,我们不给出,谁都可以实现,然后注册使用。这也是 服务者框架 的含义。JDBC 就是这么一个思想的服务者框架。关于服务者框架看这里

缺点:

  1. 静态工厂方法与构造器比起来,没有 public 或者 protected 修饰的构造器,无法实现继承

    例如,上面提到的 Collections ,就无法被继承,我们就不能继承其中的任何一个便利的实现。这或许,也是一种对使用组合而非继承的鼓励。

  2. 静态工厂方法与构造器比起来,它们不容易被程序员所知晓

    在文档中,它们不像构造器那么显眼,在上面单独的列出来,基于这个原因,要想知道如何实例化一个类,使用静态工厂方法比使用构造器相比,前者是比较困难的,因为文档中,静态工厂方法和其他静态方法没啥区别,没有做特殊处理。java 文档工具或许在未来会注意到这个问题,对静态工厂方法多给予一些关注。

    同时,我们可以在文档中对静态工厂方法的名字做一些特殊处理,遵守常见的命名规范,来减少这个问题,比如像下面提到的几个规范:

    1. from类型转换方法,根据一个单一传入的参数,返回一个对应的类型,比如:Date d = Date.from(instant);
    2. of聚合方法,接受多个参数,返回一个合并它们的实例。比如:Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
    3. valueOf比 from 和 of 更加详细 。比如:BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
    4. instance or getInstance获取实例方法,根据传入的参数,返回实例,但是不保证返回的实例完全一样,根据传入的参数不同而不同。比如:StackWalker luke = StackWalker.getInstance(options);
    5. create or newInstance获取新的实例,每次都返回新创建的实例。比如:Object newArray = Array.newInstance(classObject, arrayLen);
    6. getType ,和 getInstance 类似,用于返回的类型,不是静态工厂方法所在的类,而是其他类型。比如:FileStore fs = Files.getFileStore(path);
    7. newTypenewInstance 类型,同样用于返回的类型,不是静态工厂方法所在的类,而是其他类型。比如:BufferedReader br = Files.newBufferedReader(path);
    8. typegetType and newType 的简化版。比如:List<Complaint> litany = Collections.list(legacyLitany);

总结下,静态工厂方法和 public 构造器都有自己的优点,了解它们各自的优点是有帮助的。大部分情况下,静态工厂方法更占优势,所以,我们应该避免第一反应就使用构造器,而是先考虑下静态工厂方法。



  1. 这里的静态工厂方法,和设计模式中的静态工厂模式,很相似,但是设计模式中的静态工厂模式,它是对外隐藏对象的实现细节,通过一个工厂,根据不同的输入,产生不同的输出。本条目中的工厂,只会产生特定的输出,即自己的实例。二者还是不同的。 ↩︎

  2. 方法签名,指的是方法名字,以及方法参数列表,包括方法参数的顺序。 ↩︎

  3. 类似于单例模式的饿汉式 ↩︎

  4. 类似于单例模式的懒汉式 ↩︎

  5. 享元模式,23种设计模式中的一种, 它针对每一种内部状态仅提供一个对象,设置不同的外部状态,产生多种不同的形态,但是其内部状态对象只有一个,达到对象复用的目的。 ↩︎

  6. 这里的 Tyle 类型,不是泛型的 Type ,只是一种代指,跟 xxx 一个意思,表示 1.7 以前,对于面向接口编程的时候,想要返回协变类型,常规的做法,是写一个不可实现类 ,类的名字,就是接口的名字,多加一个 s↩︎

effective java 3th item1:考虑静态工厂方法代替构造器的更多相关文章

  1. Effective Java 读书笔记(一):使用静态工厂方法代替构造器

    这是Effective Java第2章提出的第一条建议: 考虑用静态工厂方法代替构造器 此处的静态工厂方法并不是设计模式,主要指static修饰的静态方法,关于static的说明可以参考之前的博文&l ...

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

    类可以提供一个静态方法,返回类的一个静态实例,如Boolean包装类的一个获取实例的静态方法 public static Boolean valueOf(boolean b) { return (b ...

  3. Effective java读书札记第一条之 考虑用静态工厂方法取代构造器

    对于类而言,为了让client获取它自身的一个实例,最经常使用的方法就是提供一个共同拥有的构造器. 另一种放你发,也应该子每一个程序猿的工具箱中占有一席之地.类能够提供一个共同拥有的静态 工厂方法.它 ...

  4. 【读书笔记 - Effective Java】01. 考虑用静态工厂方法代替构造器

    获取类的实例有两种方法: 1. 提供一个公有的构造器(最常用). 2. 提供一个公有的静态工厂方法(static factory method). // 静态工厂方法示例 public static ...

  5. Java - 用静态工厂方法代替构造器

    Effective Item - 考虑用静态工厂方法代替构造器我们有两种常见的方法获得一个类的实例: 公有的构造器 提供静态工厂方法(static factory method) 相对公有的构造器,静 ...

  6. 改善JAVA代码01:考虑静态工厂方法代替构造器

    前言 系列文章:[传送门]   每次开始新的一本书,我都会很开心.新书新心情. 正文 静态工厂方法代替构造器 说起这个,好多可以念叨的.做了一年多的项目,慢慢也有感触. 说起构造器 大家很明白,构造器 ...

  7. 高效JAVA之用静态工厂方法代替构造器

    程序员这行干的久了,总会染上一些恶习,我就染上一个让人深恶痛绝,自己却津津乐道的习惯,还不想改的那种,它可以叫做强迫症,也可以叫做洁癖.那就是我不允许我的IDEA出现一点点警告,什么黄色背景,绿色波浪 ...

  8. 静态工厂方法VS构造器

    我之前已经介绍过关于构建者模式(Builder Pattern)的一些内容,它是一种很有用的模式用于实例化包含几个属性(可选的)的类,带来的好处是更容易读.写及维护客户端代码.今天,我将继续介绍对象创 ...

  9. Tips1:考虑用静态工厂方法代替构造器

    用静态工厂方法来代替构造器为外界提供对象 描述: 静态工厂方法代替构造器来给外界提供对象,创建对象依然是由构造器来完成的 创建对象和提供对象: 创建对象的方式: 构造器 提供对象来哦方式: 构造器 类 ...

随机推荐

  1. Intellij IDEA 出现“Usage of API documented as @since 1.8+”的解决办法

    转自 https://blog.csdn.net/qq_27093465/article/details/69372028 具体报错内容如下: This inspection finds all us ...

  2. golang "[]uint8" to string

    关于Uinit8和Byte: The Go Programming Language Specification Numeric types uint8 the set of all unsigned ...

  3. spark 源码分析之十一--Spark RPC剖析之TransportClient、TransportServer剖析

    TransportClient类说明 先来看,官方文档给出的说明: Client for fetching consecutive chunks of a pre-negotiated stream. ...

  4. D3学习之画布制作

    最近大半个月都和d3斗争,学习艰辛(呜呜……)如果觉得作者写的对你有用,可以打赏作者哦!owo 起言:结合自己的学习之路,我认为要想使用d3画图搞清楚布局很重要,层次分明,就给了你很大的灵活性,写起代 ...

  5. 【CocoaPods】ERROR: While executing gem ... Gem::DependencyError

    今天安装 CocoaPods 时遇到了这个问题. ERROR: While executing gem ... (Gem::DependencyError) Unable to resolve dep ...

  6. React Hooks 深入系列 —— 设计模式

    本文是 React Hooks 深入系列的后续.此篇详细介绍了 Hooks 相对 class 的优势所在, 并介绍了相关 api 的设计思想, 同时对 Hooks 如何对齐 class 的生命周期钩子 ...

  7. Android Studio项目/Flutter 案例中Gradle报错通用解决方案(包括Unable to tunnel through proxy问题)

    目录 Step 1:修改Gradle版本为本地版本 Step 2:修改classpath为Android Studio版本 Step 3:关闭代理 Step 1:修改Gradle版本为本地版本     ...

  8. Js面向对象构造函数继承

    构造函数继承 <!-- 创建构造函数 --> function Animal(){ this.species= '动物'; } function Dog(name,color){ this ...

  9. spring 的权限控制:security

    下面我们将实现关于Spring Security3的一系列教程. 最终的目标是整合Spring Security + Spring3MVC 完成类似于SpringSide3中mini-web的功能. ...

  10. LFS8.3BOOT引导疑点解决

    LFS系统 的BOOT引导 在LFS书中写到的BOOT引导,时直接将宿主机的BOOT分区挂载当LFS的BOOT分区中,虽然这样也是可以实现BOOT引导的,但是我并不想这样做,所以BOOT引导就变得有些 ...