您的位置: 首页 > 技术专栏

Java高效编程:考虑静态工厂方法而不是构造函数

来源:华信智原  |  发布时间:2021-07-20  |  浏览量:272

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

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

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

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

  静态工厂方法的一个优点是,它们是有名称的,而构造函数的名称都是一样的。如果构造函数的参数本身不能描述返回的对象,那么使用一个精心命名的静态工厂更容易使用,并且生成的客户端代码更容易阅读。例如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<Rank> 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<Complaint> litany = Collections.list(legacyLitany);

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


相关新闻

24小时报名热线

400-7777-699

报名热线:400-7777-699

微博

微信公众号

友情链接 :智原在线   美味学院   安徽新华电脑   安徽新华教育

华信智原(官网)|京ICP备09028087号-8|咨询热线:400-7777-699|地址:北京海淀区北三环中路44号院爱工场文化教育产业园|版权所有:北京华信智原教育技术有限公司
在线报名 学费详情 开班信息 职业护航 视频下载

小小华想和您聊一聊