Effective Java 3rd Edition -- Consider static factory methods instead of constructors

旧城等待, 2022-05-29 07:40 181阅读 0赞

一个类允许客户获得一个实例的传统方式是提供一个公有构造器。还有另一种技术也应该在每个程序员的工具箱种占有一席之地。类可以提供一个公有的静态工厂方法(static factory method),它只是一个用于返回类的静态实例的方法。下面是一个来自 Boolean(基本类型 boolean 的封装类)的简单示例。这个方法将 boolean 基本类型转换为 Boolean 对象引用:

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

注意,静态工厂方法与设计模式[Gamma95]中的工厂方法模式不同。本条目中的这个静态工厂方法并不直接对应于设计模式中的工厂方法。

类可以通过静态工厂方法提供其客户端,而不是通过构造器。提供静态工厂方法而不是公共构造器有利也有弊。

静态工厂方法的其中一个优势在于,与构造器不同,它们有名称。如果构造其的参数本身并不描述返回的对象,则具有适当名称的静态工厂更易于使用,产生的客户端代码更易于阅读。例如,构造器 constructor BigInteger(int, int, Random) 返回的 BigInteger 可能为素数,如果用名为 BigInteger.probablePrime 的静态工厂方法来表示,显然更为清楚。(这个方法在 Java 4 中被加入)

一个类只能有一个带指定签名的构造器。编程人员通常知道如何避开这一限制:通过提供两个构造器,它们的参数列表旨在参数类型的顺序上有所不同。实际上这并不是个好主意。面对这样的 API,用户永远也记不住改用哪个构造器,结果常常会调用错误的构造器。并且,读到使用了这样构造器的代码时,如果没有参考类的文档,往往不知所云。

由于静态工厂方法有名称,所以它们不受任何上述的限制。当一个类需要多个带有相同签名的构造器时,就用静态工厂方法代替构造器,并且慎重地选择名称以便突出他们之间地区别。

静态工厂方法的第二个优势在于,与构造器不同,不需要每次调用时都创建一个新的对象。这就允许不可变类(条目 17)使用预构造的实例。或者吧已经构造好的实例缓存起来,以后再把这些实例分发给客户,从而避免创建不必要的重复对象。Boolean.valueOf(boolean)方法说明了这项技术:它从来不创建对象。这项技术类似享元模式[Gamma 95]。如果一个程序要频繁地创建相同的对象,并且创建对象的代价很高,则这项技术可以极大地提高性能。

静态工厂方法可以重复的调用返回同一个对象,这u额可以被用来控制“在某一时刻那些实例应该存在”。这样做的类被称作是实例控制的。有几个理由去写这种实例控制的类。实例控制允许一个类保证它是一个单例(条目 3)或者不可实例化的(条目 4)。同样,它也允许一个非可变类(条目 17)确保没有两个相同实例存在。当且仅当 a == b 的时候才有 a.equals(b)。这是享元模式的基础[Gamma 95]。枚举类型(条目 34)提供这种保证。

静态工厂方法的第二个优势在于,不像构造器,它们可以返回一个元返回类型的子类的对象。这样我们在选择被返回对象的类型时就有了更大的灵活性。

这种灵活性的一个应用是,一个 API 可以返回一个对象,同时又不使该对象的类型称为共有的。以这种方式把具体的实现类隐藏起来,可以得到一个非常简洁的 API。这项技术非常适用于基于接口的框架(条目 20),因为在这样的框架结构中,接口称为静态工厂方法的自然返回实例。

在 Java 8 之前,接口不能有静态方法。按照惯例,名为 Type 的接口的静态工厂方法被放入一个名为 Types 的不可实现的伴随类(条目 4)中。例如:Java
Collections Framework 有 45 个几口的实用程序实现,提供了非可修改集合、同步集合等。这些实现绝大多数都是通过一个不可实例化的类(java.util.Collections)中的静态工厂方法而被导出的。返回的对象的类都是非公有的。

Collections Framework API 比导出 45个独立的公有类要小得多,这不仅仅是指 API 数量上的减少,而且也是概念意义上的减少:编程人员为了使用 API 必须掌握的概念的数量和难度。编程人员知道,被返回的对象是由相关接口精确指定的,所以它们不需要阅读有关实现类的类文档。更进一步,使用这样的精要工厂方法,可以强迫客户通过接口来引用被返回的对象,而不是通过实现类来引用被返回的对象,这是一个很好的习惯(条目 64)。

从 Java 8 开始,接口不能包含静态方法的限制被消除了,所以通常没有理由为接口提供不可实例化类的伴随类。很多公开的静态成员应该放在这个接口本身中。但是,请注意,人有必要将这些静态方法背后的大部分实现代码放在单独的包-私有类中。这是因为 Java 8 要求所有接口的静态成员都是公开的。Java 9 允许私有静态方法,但静态字段和静态成员仍然需要公开。

静态工厂的第四个优点是,返回对象的类可以根据输入参数的不同而不同。声明的返回来行的任何子类型都是允许的。返回对象的类也可能引发不版本而异。

EnumSet 类(条目 36)没有公有构造函数,只有静态工厂。在 OpenJDK 视线中,它们根据基本美剧类型的大小返回两个子类之一的实例:如果它有 64 个或少量元素
The EnumSet class (Item 36) has no public constructors, only static factories.In the OpenJDK implementation, they return an instance of one of two subclasses,depending on the size of the underlying enum type: if it has sixty-four or few erelements, as most enum types do, the static factories return a RegularEnum Setinstance, which is backed by a single long; if the enum type has sixty-five or more elements, the factories return a JumboEnumSet instance, backed by a long array.

这两个实现类的存在对客户是不可见的。如果 RegularEnumSet 不再为小型枚举类型提供性能优势,则可以在未来版本中将其淘汰,而不会产生不良影响。

同样,未来的版本可能会添加 EnumSet 的第三个或第四个实现,如果它证明对性能有益。客户及不知道也不关心它们从工厂回来的对象的类;它们只关心它是 EnumSet 的子类。

静态工厂的第五个优点是,当包含方法的类被写入时,返回对象的类不需要存在。这种灵活的静态工厂方法构成了服务供应商框架的基础,如 Java Database Connectivity API(JDBC)。服务提供者框架时供应商实现服务的系统,并且系统使得实现对客户端可用,从而将客户端与实现解耦。

服务供应商框架中有三个基本组件:一个代表实现的服务接口;供应商注册 API,供应商用于注册实施;和一个服务访问 API,客户用它来获取服务的实例。服务访问 API 可以允许客户指定选择实现的标准。在没有这样的标准的情况下,API 返回默认实现的实例,或者允许客户循环遍历所有可用的实例。服务访问 API 时构成服务提供商框架基础的灵活的静态工厂。

服务提供商框架的可选第四个组件时服务提供商接口,它描述了生成服务接口实例的工厂对象。在没有服务提供商接口的情况下,实现必须反射性(条目 65)地实例化。在 JDBC 的情况下,Connection 扮演服务接口的一部分,DriverManager.registerDriver 时提供商注册 API,DiverManager.getConnection 是服务访问 API,Driver 是服务提供商接口。

服务提供商框架模式有许多变种。例如:服务访问 API 可以向客户端返回比提供商提供的更丰富的服务接口。这就是桥梁模式[Gamma 95]。依赖注入框架(条目 5)可以被视为强大的服务提供者。自 Java 6 依赖,该平台包含一个通用服务提供商框架 java.util.ServiceLoader,因此你不需要,一般也不应编写自己的(条目 59)。JDBC 不是用 ServiceLoader,因为前者早于后者。

仅提供静态工厂方法的主要限制是没有公有或受保护的构造函数的类不能被子类化。例如,不可能在集合框架中对任何便利实现类进行子类化。可以说,这可能是一个伪装的祝福,因为它鼓励程序员使用组合而不是继承(条目 18),并且对非可变类型(条目 17)是必须的。

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

  • from—类型转换方法,它接收单个参数并返回此类型的相应实例,例如:Date d = Date.from(instant);
  • of—一个接收多个参数并返回包含它们的此类型实例的聚合方法,例如:Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
  • valueOf—比 from 和 of 更为详尽的替代方法,例如: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—getType 和 newType 的简洁替代方法,例如:List<Complaint> litany = Collections.list(legacyLitany);

总之,静态工厂方法和公有构造函数都有它们的用途,并且了解它们的相对优点是值得的。通常情况下,静态工厂是可取的,所以避免反思,在没有首先考虑静态工厂的情况下提供公共构造函数。

发表评论

表情:
评论列表 (有 0 条评论,181人围观)

还没有评论,来说两句吧...

相关阅读