资讯专栏INFORMATION COLUMN

第1项:考虑静态工厂方法而不是构造函数

赵连江 / 3126人阅读

摘要:提供静态工厂方法而不是公共构造函数既有优点也有缺点。它们不像构造函数那样在文档中脱颖而出,因此很难弄清楚如何实例化提供静态工厂方法而不是构造函数的类。

  类允许客户端获取实例的传统方法是提供公共构造器。还有一种技术应该是每个程序员的工具箱的一部分。一个类可以提供一个公共静态工厂方法,它仅仅是一个返回类实例的静态方法。下面是布尔(布尔型的盒装原语类)的一个简单示例。这个方法将一个布尔原始值转换成布尔对象引用:

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

  注意: 静态工厂方法与设计模式 [Gamma95] 的工厂方法模式不同,在这个Item中描述的静态工厂方法并不等同于设计模式中的工厂方法模式。

  一个类可以为其客户提供静态工厂方法,而不是公共构造函数。提供静态工厂方法而不是公共构造函数既有优点也有缺点。

  静态工厂方法的一个优点是,它们是有名称的,而构造函数的名称都是一样的。 如果构造函数的参数本身不能描述返回的对象,那么使用一个精心命名的静态工厂更容易使用,并且生成的客户端代码更容易阅读。例如BigInteger的一个构造函数:BigInteger(int, int, Random),这个构造函数返回一个BigInteger有可能是一个质数,使用一个精心命名的静态工厂方法会更容易描述该方法返回的BigInteger类型,比如BigInteger.probablePrime(这是在Java 4 中添加的)。

  一个类只能有一个带有给定签名的构造函数。程序员可以通过提供两个构造函数来绕过这个限制,这些构造函数的参数列表只在参数类型的顺序上有所不同。这是个相当坏的主意。这样使用这个API的用户永远无法记住应该使用哪个构造函数,并且最终会调用错误的构造函数。使用这些构造函数的人在不阅读的引用类文档的情况下是不知道代码是干什么的。

  因为构造函数有名称,所以就不会有这个限制,在一个类需要使用多个签名、多个构造函数的情况下,用静态工厂方法代替构造函数,并仔细选择方法的名称就可以突出构造函数之间的差异了。

  静态工厂方法的第二个优点是,与构造函数不同,它们不需要在每次被调用时创建一个新对象。 这允许不可变类(第17项)使用预先构造的实例,或者在构建时缓存实例,并重复分发它们,以避免创建不必要的重复对象。Boolean.valueOf(boolean)方法使用了这种方式:它从不创建对象。这种技术类似于享元模式[Gamma95]。如果经常请求等效对象,特别是当它们的创建成本很高时,它可以极大地提高性能。

  静态工厂方法从重复调用中返回相同的对象的能力允许类在任何时候保持对实例的严格控制。这样做的类被认为是实例控制的。编写实例控制类的原因有几个。实例控制允许一个类保证它是一个单例(第3项)或非实例化的(第4项),并且它允许一个不可值的类(第17项)来保证没有两个相等的实例存在:当且仅当a==b成立时,a.equals(b)返回true,这是享元模式 [Gamma95] 的基础,枚举类型就提供了这种保证。

  静态工厂方法的第三个优点是,与构造函数不同,它们可以的对象可以是返回类型的任何子类的实例对象。 这使在选择返回的对象的类时具有很大的灵活性。

  这种灵活性的一个应用是,API可以返回对象,同时又不会使对象的类编程公有的,以这种方式隐藏实现类会使API变得非常简洁。这种技术适用于基于接口的框架(interface-based frameworks,见第20项),因为在这种框架中,接口为静态工厂方法提供了自然返回类型。

  在Java 8 之前,接口不能有静态方法。按照惯例,名为Type的接口的静态工厂方法被放置在一个不可实例化的名为Types的配套类(noninstantiable companion class)(第4项)中。例如,Java Collections Framework有45个便利实现,分别提供了不可修改的集合、同步集合等等。几乎所有这些实现都通过静态工厂方法在一个不可实例化的类(java.util.Collections)中导出。所有返回对象的类都是非公有的。

  现在的Collections Framework API比导出的45个独立的公有类的那种实现方式要小得多,每种便利的实现都对应一个类。这不仅仅减少了API的数量,还包括概念上的权重:程序猿必须掌握的概念的数量和难度,以便使用API。程序猿知道返回的对象正好有其接口指定的API,因此不需要为实现类去阅读额外的类文档。此外,这种工厂方法要求客户端通过接口而不是实现类来引用返回的对象,这通常是很好的实践方式(第64项)。

  从Java 8开始,消除了接口不能包含静态方法的限制,因此通常没有理由为接口提供不可实例化的伴随类。许多公共静态成员应该放在接口本身中。但请注意,可能仍有必要将大量实现代码放在这些静态方法后面的多带带的包私有类中。这是因为Java 8要求接口的所有静态成员都是公共的。 Java 9允许私有静态方法,但静态字段和静态成员类的属性依然是要求是公共的。

  静态工厂方法的第四个优点是,静态工厂方法所返回的对象的类可以随着每次调用而变化,这取决于静态工厂方法的参数值。 只要返回的类型是声明的类的子类都是允许的。返回对象的类也可能随着发行版本的不同而不同。

  在EnumSet类(第36项)中有非公有的构造方法,只有静态工厂方法。在 OpenJDK 实现中的,它们返回两个子类之一的实例,具体取决于基础枚举类型的大小: 如果它有64个或更少的元素,就像大多数枚举类型所做的那样,静态工厂返回一个RegularEnumSet实例, 它由单个long的支持;如果枚举类型有65个或更多元素,则工厂将返回一个由长数组支持的JumboEnumSet实例。

  这两个实现类的存在对于客户端是不可见的。 如果 RegularEnumSet 不再为小枚举类型提供性能优势可以从未来版本中删除,没有任何不良影响。 同样,未来如果证明有利于性能,则可以添加EnumSet的第三或第四个实现。客户即不知道也不关心他们从工厂中获取的对象的类型,他们只关心它是EnumSet的一些子类。

  静态工厂方法的第五个优点是,返回的对象所属的类,在编写包含该静态工厂方法的类时可以不必存在。 这种灵活的静态工厂方法构成了服务提供者框架(Service Provider Framework)的基础,例如JDBC(Java数据库连接,Java Database Connectivity)API。服务提供者框架是提供者实现服务的系统,系统使实现可用于客户端,将客户端与实现分离【微服务】。

  服务提供者框架中有三个基本组件:服务接口【提供者】,代表一个实现;提供者注册API【注册中心】,提供者用于注册实现; 以及服务访问API【消费者】,客户端使用它来获取服务的实例。 服务访问API可以允许客户端指定用于选择实现的标准。 如果没有这样的标准,API将返回默认实现的实例,或允许客户端循环遍历所有可用的实现。 服务访问API是灵活的静态工厂,它构成了服务提供者框架的基础。

  服务提供者框架的可选第四个组件是服务提供者接口,它描述了生成服务接口实例的工厂对象。 在缺少服务提供者接口的情况下,必须反复实例化实现(第65项)。 对于JDBC,Connection扮演服务接口的一部分,DriverManager.registerDriver是提供者注册API,DriverManager.getConnection是服务访问API,Driver是服务提供者接口。

  服务提供者框架模式有许多变体。 例如,服务访问API可以向客户端返回比提供者提供的服务接口更丰富的服务接口。 这是桥接模式 [Gamma95] 。 依赖注入框架(第5项)可视为强大的服务提供者。 从Java 6开始,该平台包含一个通用服务提供程序框架java.util.ServiceLoader,因此您不需要(通常不应该)自己编写(第59项)。 JDBC不使用ServiceLoader,因为前者早于后者。

  静态工厂方法的主要限制在于,类如果不含公有的或者受保护的构造器,就不能被子类化。 例如:不可能将Collections Framework中的任何方便的实现类子类化。但是这也许会因祸得福,因为它鼓励程序猿使用组合,而不是继承(第18项),并且要求必须是不可变的(第17项)。

  静态工厂方法的第二个缺点是程序员很难找到它们。 它们不像构造函数那样在API文档中脱颖而出,因此很难弄清楚如何实例化提供静态工厂方法而不是构造函数的类。 Javadoc工具有一天可能会引起对静态工厂方法的注意。 在此期间,您可以通过引起对类或接口文档中的静态工厂的注意并遵守常见的命名约定来减少此问题。 以下是静态工厂方法的一些常用名称。 这份清单远非详尽无遗:

from:一种类型转换方法,它接受单个参数并返回此类型的相应实例,例如:Date d = Date.from(instant);

of:一种聚合方法,它接受多个参数并返回包含它们的此类型的实例,例如:Set faceCards = EnumSet.of(JACK, QUEEN, KING);

valueOf:一个更详细的替代方案,例如:BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);

instance or getInstance:返回由其参数(如果有)描述的实例,但不能说它具有相同的值,例如:StackWalker luke = StackWalker.getInstance(options);

create or newInstance:与instance或getInstance类似,不同之处在于该方法保证每个调用都返回一个新实例,例如:Object newArray = Array.newInstance(classObject, arrayLen);

getType:与getInstance类似,是在工厂方法位于不同的类中时使用它。 Type指的是工厂方法返回的对象类型,例如:FileStore fs = Files.getFileStore(path);

newType:与newInstance类似,是在工厂方法位于不同的类中的时候使用。Type指的是工厂方法方位的对象类型,例如:BufferedReader br = Files.newBufferedReader(path);

type:获取Type和new Type一个简明替代的方法,比如:List litany = Collections.list(legacyLitany);

  总之,静态工厂方法和公共构造函数都有它们的用途,理解它们的相对优点是值得的。 通常静态工厂是优选的,不要在第一反应就是使用构造函数,应当先考虑使用静态工厂方法。


关注公众号获取同步更新

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/73876.html

相关文章

  • 6:避免创建不需要的对象

    摘要:传递给构造器的参数本身就是一个实例,功能方面等同于构造器创建的所有对象。对于同时提供了静态工厂方法第项和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,这样可以经常避免创建不必要的对象。   一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象。重用的方式既快速,有流行。如果对象是不可变(immutable)的(第17项),那么就能重复使用它。   作为一个...

    Amio 评论0 收藏0
  • 二章 创建和销毁对象

    摘要:一个类可以提供一个公共静态工厂方法,它仅仅是一第项遇到多个构造器参数时要考虑使用构建器静态工厂和构造器有个共同的局限性他们都不能很好地扩展到大量的可选参数。   本章涉及创建和销毁对象,包括何时以及如何创建它们,何时以及如何避免创建它们,如何确保它们被及时销毁,以及如何管理在销毁之前必须进行的清理操作。 第1项:用静态工厂方法代替构造器   类允许客户端获取实例的传统方法是提供公共构造...

    Jeffrrey 评论0 收藏0
  • 2:当面临多个参数的构造器时考虑使用构建器

    摘要:因此,最好一开始就考虑使用构造器。与使用传统的重叠构造器模式相比,使用模式的客户端代码更易于阅读和编写,构建器也比更加安全。   静态工厂和构造器有个共同的局限性:他们都不能很好地扩展到大量的可选参数。考虑用一个类表示包装食品外面显示的营养成分标签。这些标签中有几个域是必需的:每份的含量、每罐的含量以及每份的卡路里,还有超过20个可选域:总脂肪、饱和脂肪量、转化脂肪、胆固醇、钠等等。大...

    KavenFan 评论0 收藏0
  • Effective Java 3rd.Edition 翻译

    摘要:推荐序前言致谢第一章引言第二章创建和销毁对象第项用静态工厂方法代替构造器第项遇到多个构造器参数时要考虑使用构建器第项用私有构造器或者枚举类型强化属性第项通过私有构造器强化不可实例化的能力第项优先考虑依赖注入来引用资源第项避免创建不必要的对象 推荐序 前言 致谢 第一章 引言 第二章 创建和销毁对象 第1项:用静态工厂方法代替构造器 第2项:遇到多个构造器参数时要考虑使用构建器 第...

    KoreyLee 评论0 收藏0
  • Effective Java 三版 全文翻译

    摘要:本章中的大部分内容适用于构造函数和方法。第项其他方法优先于序列化第项谨慎地实现接口第项考虑使用自定义的序列化形式第项保护性地编写方法第项对于实例控制,枚举类型优先于第项考虑用序列化代理代替序列化实例附录与第版中项目的对应关系参考文献 effective-java-third-edition 介绍 Effective Java 第三版全文翻译,纯属个人业余翻译,不合理的地方,望指正,感激...

    galois 评论0 收藏0

发表评论

0条评论

赵连江

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<