一个类创建一个实例最传统的方式是使用构造方法。
public class Student {
private String name;
private int age;
//constructor
public Student(){
}
public Student(String name,int age){
this.name=name;
this.age=age;
}
}
public static void main(String[] args) {
//use constructor to create Student instance
Student student1=new Student();
Student student2=new Student("student2",18);
}
现在开始介绍一个应该成为每个程序员都应该掌握的技巧,它就是静态工厂方法(static factory method)。它不过是一个返回一个类的实例的静态方法。这是一个关于Boolean的简单例子。这个方法将一个boolean原始类型的值转化为Boolean类型的引用对象:
public static Boolean valueOf(boolean b){
return b?Boolean.TRUE:Boolean.FALSE;
}
需要注意的是,一个静态工厂方法与“设计模式”的工厂模式(Factory Method)是不一样的。在本条目描述的静态工厂方法与设计模式的是不太一样的。
一个类可以提供它的客户端静态工厂方法,或者公开的构造方法。使用静态工厂方法代替公有构造方法有优点也有缺点。
使用静态工厂方法的好处之一是,不像构造方法,它们有名字。
//constructor
public Student(){
}
public Student(String name,int age){
this.name=name;
this.age=age;
}
//static factory method
public Student createInstance(){
return new Student();
}
如果一个构造方法有零个或一个或多个参数,只能用对象的返回类型来描述它们的作用,而良好命名的静态工厂方法更容易使用,同时也导致代码更加容易阅读。举个例子,构造方法BigInteger(int ,int ,Random)的功能是返回一个BigInteger类型的质数。但是用静态工厂方法能更好地描述这个功能:BigInteger.probablePrime(该方法已在Java4中添加)。
一个类只能有一个特定类型的构造方法。程序员一直知道如何规避这个限制,那就是提供两个仅仅是参数顺序不一样的构造方法。这真是一个坏主意。
这样是报错的
这样就没有问题
使用这样的API的使用者永远不会知道哪个构造方法是哪个,最终可能调用错误的那一个导致犯错。人们阅读代码的时候也不知道这些构造方法是在做什么,除非他们查阅类的文档。
因为静态工厂方法有名字,所以使用它们不会出现上文提到的问题。在遇到一个类看起来需要使用多个构造方法的时候,你应该考虑使用静态工厂方法来代替。同时你应该好好考虑这些方法的命名,体现出他们的不同。
使用静态工厂方法的第二个好处是不像构造方法,无需每次调用时都创建一个新对象。这个特性允许不可变类(item17)使用预先构造的实例或者缓存已被构造的实例使之重复使用,从而避免创建没有必要的重复对象。方法Boolean.valueOf(boolean)很好的解释了这个技巧:它从不创建对象。这个技巧与享元模式(FlyWeight)类似。如果一个相等的对象需要被多次请求,特别是创建这个对象开销很大,这个技巧可以极大地提高性能。
静态工厂方法可以从重复调用返回相同对象的时候,允许类对实例在任何时候的生存周期维持着严格控制。执行这个行为的类被称为实例受控(instance-controlled)。如下表述了编写实例受控类的几个原因。实例控制保障了这个类是一个单例( item3)或者不可实例化(item4)。同时,它保障了一个不可变值类(item17)确保没有两个相等的实例存在:a.equals(b) 有且仅当 a == b。这是享元模式(FlyWeight)的基础。枚举类型(item34)提供了这个保障。
静态工厂方法的第三个优势就是,不像构造方法,静态工厂方法可以返回任何返回类型的子类型的对象。这在选择类的返回对象的时候给了你极大的灵活性。
public class Son extends Father{
}
public class Father {
/**
* 构造方法只能返回Father类型
*/
public Father(){ }
/**
* 静态工厂方法可以返回该对象类型或子类对象
*/
public static Father getInstance(Boolean isFather){
if(isFather){
return new Father();
}
return new Son();
}
}
如此灵活性的特性的其中一个应用就是,一个API返回一个对象时,无需将该返回对象的访问权限提升到public。使用如此时尚的方式隐藏了实现的类编写了紧凑的API。这个使用静态工厂方法的接口提供了不加修饰的返回类型,使之成为基于接口的框架(item20)。
在Java8之前,接口不可以有静态方法。
image.png
image.png
image.png
image.png
按照惯例,一个接口叫做Type的静态工厂方法类放置在名为Types的不可实例化随类(item4)中。打个比方,Java Collections Framework拥有四十五个接口的通用实现,提供了不可修改集合,同步集合等等。几乎所有这些实现都通过一个不可实例化的类(java.util.Collections)的静态工厂方法导出。这些返回对象的类的访问权限都不为public。
Collections Framework API比它导出的四十五个独立的公有类更小,每一个公共类对应其方便的实现。这不只是API的体积减少,还把概念权重降低了:开发者为了使用API必须精通概念的数量和复杂度。开发者知道接口返回的对象精确地详述了API的作用,所以没有必要阅读这个实现类以外的类的文档。此外,使用如此的静态工厂方法需要客户端查阅接口的返回对象而不是实现类,这是普遍的最好的做法。
从Java8开始,接口不能包含静态方法的限制已经被淘汰了,所以通常没有理由为接口提供不可实例化的伴随类。很多公有静态成员在这些类中被藏起来了,它们应该放到接口本身。但是请注意,将这些静态方法背后实现的代码的大部分放在单独的包-私有类中仍然是有必要的,这是因为Java8需要接口的所有静态成员访问权限为公有。Java9允许私有静态方法,但是静态范围和静态成员类仍然需要公有。
第四个静态工厂方法的优势是其返回的对象可以随着调用而变化,作为调用的参数。任何声明了的返回类型的子类型都是允许的。返回对象的类也因发行版而异。
EnumSet类(item36)没有公有构造器,只有静态工厂方法。在OpenJDK的实现中,通过基本枚举类型的字节大小来选择其两个子类中的一个实例返回:如果这个类有64个或更少的元素,就像绝大多数的枚举类型一样,静态工厂方法返回一个RegularEnumSet类型的实例,由单精度long的长度支持。如果这个枚举类型拥有64个或更多的元素,工厂方法返回类型为JumboEnumSet的实例,由一个long数组支持。
这两个实现类的存在对于客户端来说是不可见的。如果RegularEnumSet对小型枚举类型失去了高性能的好处,在未来某个release版本中这个类消失了也没有什么坏影响。简单地说,未来某个release版本有可能会加入第三版甚至第四版EnumSet的实现,只要对性能有提升。客户端不知道或者说根本不在乎这个工厂方法返回的对象到底是什么类,他们只关系这是一个EnumSet的子类。
第五个静态工厂方法的好处是当类包含已经写好的方法,返回的对象无需存在。如此灵活的静态工厂方法组成了服务提供框架(service provider framework)的基础。比如Java数据库连接API(JDBC)。一个服务提供框架是一个提供服务实现的系统,这个系统的实现可以被客户端利用。这把客户端的实现解耦了。
服务提供框架有三个必不可少的组件:一个服务接口(service interface),包含了实现;一个服务注册API(service registration API),提供了注册实现;以及一个服务可访问API(service access API),客户端利用这个取得服务的实例。服务可访问API可允许客户端在选择实现的时候明确规范。在缺失这个规范的情况下,这个API返回默认实现的实例,或者允许客户端循环通过所有可用的方法。这个服务可访问API是灵活的静态工厂方法,它构成了服务提供框架的基础。
一个可选择的第四个服务提供框架的组件是服务提供接口(service provider interface)。它描述了一个工厂对象提供的服务接口的实例。缺少服务提供框架的时候,实现方法必须通过反射实例化(item65)。在JDBC的情况下,Connection扮演了服务接口的角色。DriverManager.registerDriver提供了注册API,DriverManager.getConnection是服务可访问API,Driver是服务提供API。
现在有很多服务提供框架模式的变体。比如,服务可访问API相较一个提供者的提供能向客户端返回更丰富的服务接口。这就是桥接模式(Bridge pattern)。依赖注入框架(item5被视为强大的服务提供者。从Java6开始,平台包含了一个多功能的服务提供者框架,java.util.ServiceLoader,所以你不需要,并且通常也不应该,写你自己的服务提供者框架。JDBC不使用ServiceLoader,因为它比ServiceLoader出现得早。
仅提供静态工厂方法的主要限制是类在没有公有或保护的构造方法时是不可以有子类的。比如, Collection Framework的任何方便实现都不可能被子类化。可以认为这是一种伪装的祝福,因为这是为了鼓励程序员使用组件而不是继承(item18),同时,这也需要不可变的类型(item17)。
第二个静态工厂方法的短处是它们很难被开发者找到。它们在API文档中并不像构造方法一样显著。所以它很难弄清楚怎么通过提供的静态工厂方法实例化一个类而不是用构造器。Javadoc工具可能在未来的某一天会对静态工厂方法引起注意。同时,你可以通过着重注意类或接口文档中的静态方法克服这个问题,以及通过遵守常用的命名规则。这里有一些常用的静态工厂方法命名。这个名单远远不够全面。
-
from-----一个类型转换方法,传入一个参数返回一个这个类型相应的实例,比如
Date d =new Date.from(instant); -
of-----一个聚合根(aggregation)方法,传入多个参数,返回包含它们的类型的一个实例,比如:
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类型,但是用于工厂方法返回不同的类。类型是通过工厂方法返回的对象的类型,比如:
FileStore fs = File.getFileStore(path); -
newType-----和newInstance类似,但是用于在不同的类中的工厂方法。类型是通过工厂方法返回的对象的类型,比如:
BufferReader br = Files.newBufferedReader(path); -
type-----一个简明的getType和newType的替代,比如:
List<Cpmplaint> litany = Collections.list(legacyLitany);
总结,静态工厂方法和公有构造方法都有它们的适用范围,
应该去理解它们各自的优缺点。静态工厂方法通常是更好的,所以避免第一反应提供公有构造方法而不是考虑静态工厂方法。
item1-github链接
本文写于2018.9.12,历时16天