对于类而言,允许客户端获取其实例的传统方式是提供一个公有的构造器。但还有一种方法,也应该在每个程序员的工具箱中占有一席之地。类可以提供一个公有的静态工厂方法(static factory method),它只是一个返回类的实例的静态方法。下面是一个 Boolean
类型(基本类型 boolean
的包装类)的简单示例。这个方法将一个 boolean
基本类型值转换成了一个 Boolean
对象引用:
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
注意,静态工厂方法和设计模式(design patterns)中的工厂方法模式(factory method pattern)并不相同 [Gamma95]。本条目描述的静态工厂方法在设计模式中没有直接的对应项。
类可以为其客户端提供静态工厂方法,而不是公有构造器,或者说除了公有构造器之外,提供静态工厂方法而非公有构造器既有优点也有缺点。
静态工厂方法相比于构造器的第一个优点是,它们具有名称。 如果构造器的参数本身没有描述返回的对象,那么具有适当名称的静态工厂会更易于使用,客户端生成的代码也更易于阅读。例如,构造器 BigInteger(int, int, Random)
返回的 BigInteger
可能为素数,它可以更好地用一个名为 BigInteger.probablePrime
的静态工厂方法来表示。(此方法是在 Java 4 中添加的。)
一个类只能有一个具有给定签名的构造器。程序员可以通过提供两个构造器来避开这个限制,而这两个构造器的参数列表仅在于参数类型的顺序不同。不过这是个非常糟糕的主意。因为这种 API 的用户将永远无法分清该使用哪一个,最终将会导致错误的调用。在没有参考文档的情况下,当有人阅读使用了这些构造器的代码时,也无法明白其中的意图。
因为静态方法有名称,所以不受上述限制。如果某个类需要多个具有相同签名的构造器时,请使用静态工厂方法替换构造器,并且慎重地选择名称来突出它们之间的差异。
静态工厂方法相比于构造器的第二个优点是,不需要在每次被调用时都创建一个新对象。 这允许不可变类(第 17 条)使用预先构建的实例,或者缓存构造好的实例,进行重复分配,以避免创建不必要的重复对象。Boolean.valueOf(boolean)
方法就说明了这种技术:它从来不会创建对象。这种技术类似于享元模式(flyweight pattern) [Gamma95]。如果经常请求相同的对象,特别是它们的创建成本较高,则这种方法可以极大地提高性能。
静态工厂方法被重复调用时返回相同对象的能力,有助于类在任何时候严格控制哪些实例应该存在。执行此操作的类被称为实例受控的类(instance-controlled)。编写实例受控的类有几个原因。实例受控允许类保证它是一个单例(singleton 第 3 条)或是不可实例化的(noninstantiable 第 4 条)。此外,它使得不可变类(immutable value class 第 17 条)保证不存在两个相等的实例:即 a.equals(b)
为真当且仅当 a == b
为真 。这就是享元模式 [Gamma95] 的基础。枚举(enum)类型(第 34 条)保证了这一点。
静态工厂方法相比于构造器的第三个优点是,它们可以返回原返回类型的任何子类型的对象。 这使得你在选择返回对象的类时具有很大的灵活性。
这种灵活性的一个应用是,API 可以在不使得类变得公有的情况下返回其对象。以这种方式隐藏实现类会让 API 非常简洁紧凑。这种技术适用于基于接口的框架(interface-based framework 第 20 条),其中接口为静态工厂方法提供自然返回类型。
在 Java 8 之前,接口不能有静态方法。按照惯例,接口 Type 的静态工厂方法放在名为 Types 的不可实例化的伴随类中(第 4 条)。例如,Java 集合框架(java collections framework)的接口有 45 个便利的实现,提供了不可修改的集合、同步集合等。几乎所有的这些实现都是通过一个不可实例化类(java.util.Collections
)中的静态工厂方法导出的。所有返回对象的类都是非公有的。
每种便利实现都对应一个类,使得现在的集合框架 API 比由它直接导出 45 个独立的公有类的实现方式要小得多。这不仅仅减少了 API 的数量,也减轻了“概念权重”(conceptual weight):程序员为了使用 API 而必须掌握的概念的数量和难度。因为程序员知道,返回的对象是由相关接口的 API 精确指定的,所以不需要为实现类(implementation class)而阅读额外的文档。此外,使用这种静态工厂方法需要客户端通过接口而不是实现类来引用返回的对象,这通常是一种很好的做法(第 64 条)。
从 Java 8 开始,接口不能包含静态方法的限制被取消,因此,通常没有理由再为接口提供不可实例化的伴随类,许多在在这个类中的公共静态成员应该放在接口本身中。但请注意,可能仍有必要将这些静态方法背后的实现放在一个单独的包私有(package-private)类中。这是因为 Java 8 要求接口的所有静态成员都是公有的。Java 9 允许私有静态方法,但静态属性和静态成员类仍然需要公开。
静态工厂方法的第四个优点是,其作为输入参数的函数,返回对象的类可以随着调用而变化。 只要是已声明返回类型的子类型都是允许的。返回对象的类也可能因发行版本不同而有差异。
EnumSet
类(第 36 条)没有公共构造器,只有静态工厂方法。在 OpenJDK 的实现中,取决于底层枚举类型的大小,这些静态工厂方法会返回两个实现类中的一个的实例:像大多数枚举类型那样,如果元素数目为 64 个或更少,静态工厂方法会返回一个 RegularEnumSet
实例,由单一的 long
提供支持;如果枚举类型包含 65 个或更多元素,则工厂会返回一个由 long
数组支持的 JumboEnumSet
实例。
这两个实现类的存在对于客户端来说是不可见的。如果 RegularEnumSet
不能再为小型枚举类型提供性能优势,可以从未来的发行版本中去掉,这没有任何不良影响。同样,如果被证明对性能是有益的,未来的发行版本还可以添加 EnumSet
的第三或第四个实现。客户既不需要知道也不必关心他们从工厂方法获得的对象的类,只要知道它是 EnumSet
的子类即可。
静态工厂方法的第五个优点是,当编写包含该静态工厂方法的类时,返回对象所属的实现类不需要存在。 这种灵活的静态工厂方法构成了服务提供者框架(service provider framework)的基础,如 Java 数据库连接 API(JDBC)。服务提供者框架是多个服务提供者实现一个服务的系统,该系统为客户端提供多个适用的实现,并将客户端与实现解耦。
服务提供者框架中有三个重要的组件:首先是服务接口(Service Interface),它代表服务的一个实现; 其次是提供者注册 API(Provider Registration API),用于提供者注册服务的实现;最后是服务访问 API(Service Access API),客户端使用它来获取服务的实例。服务访问 API 一般允许客户端指定选择实现的标准。在没有这种标准的情况下,API 将返回默认实现的一个实例,或是允许客户端循环遍历所有可用的实现。服务访问 API 即是灵活的静态工厂,它构成了服务提供者框架的基础。
服务提供者框架的第四个组件服务提供者接口(Service Provider Interface)是可选的,它描述了一个生成服务接口实例的工厂对象。在没有服务提供者接口的情况下,服务的实现必须通过反射的方式进行实例化(第 65 条)。对于 JDBC 来说,Connection
扮演了服务接口的一部分,DriverManager.registerDriver
是提供者注册API,DriverManager.getConnection
是服务访问API,而 Driver
就是服务提供者接口。
服务提供者框架模式有许多变体。例如,服务访问 API 可以向客户端返回比提供者所提供的更丰富的服务接口。桥接模式(bridge pattern)就起到了这样的作用 [Gamma95]。依赖注入框架(dependency injection framework 第 5 条)可视为一个强大的服务提供者。从 Java 6 开始,该平台包含了一个通用(general-purpose)服务提供者框架:java.util.ServiceLoader
,因此你不需要(通常不应该)编写自己的服务提供者框架(第 59 条)。JDBC 早于 ServiceLoader
,所以 JDBC 没有使用该框架。
仅提供静态工厂方法的主要限制是,类如果没有公有或受保护的构造器,就不能被子类化(继承)。 例如,子类化集合框架(Collections Framework)中的任何便捷实现类都是不可能的。但这可能因祸得福,因为它鼓励程序员使用组合(composition)而不是继承(第 18 条),对于不可变类型(第 17 条)而言,这是必需的。
静态工厂方法的第二个缺点是,程序员很难找到它们。 在 API 文档中,它们并不像构造器这样突出,因此,很难弄清楚如何实例化一个只提供静态工厂方法而没有构造器的类。Javadoc 工具有一天可能会关注静态工厂方法。在此期间,你可以通过关注类或接口文档中的静态工厂方法,并遵循常见的命名约定来弥补这一劣势。以下是静态工厂方法的一些常用命名。当然,这份清单远非详尽:
-
from —— 一种类型转换方法(type-conversion method),它接收单个参数并返回此类型的相应实例,例如:
Date d = Date.from(instant);
-
of —— 一种聚合方法(aggregation method),它接受多个参数,返回包含这些参数的该类型的实例,例如:
Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
-
valueOf —— 较
from
和of
更为详细的可选方案,例如:BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
-
instance 或 getInstance —— 返回由其参数(如果有)描述的实例,但不能说实例与参数具有相同的值,例如:
StackWalker luke = StackWalker.getInstance(options);
-
create 或 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);
总之,静态工厂方法和公有构造器都有用处,理解它们各自的优缺点是值得的。通常,静态工厂更可取,因此在没有优先考虑静态工厂的情况下,要避免习惯性地提供公有构造器。
[Gamma95]
Gamma, Erich, Richard Helm, Ralph Johnson, and John Vlissides. 1995.
Design Patterns: Elements of Reusable Object-Oriented Software.
Reading, MA: Addison-Wesley. ISBN: 0201633612.
翻译:Angus
校对:Inno