允许客户端创建一个实例的传统方法是:提供一个公共构造函数;有另外一个必须成为每个程序员的编程技巧:优先使用类提供的只简单返回实例的公共静态工厂方法来创建对象。这有一个简单的Boolean类的例子:这个方法转换一个原生的boolean类型的值为一个Boolean的实例。
public static Boolean valueOf(boolean b){ return b?Boolean.TRUE:Boolean.FALSE; }
注意这一点:类的静态工厂方法不同于设计模式的工厂设计模式。本条规约跟设计模式没有关联。
一个类可以通过静态工厂方法或者使用公共构造函数提供给自己的客户端调用来创建实例,使用静态工厂方法而不是公共构造函数有优点也有缺点。
优点1:它有名字,可读性更好;
构造函数的参数跟自己无关,没有描述返回对象;而良好命名的静态工厂方法客户端代码可读性更好,所以更容易使用;
//使用构造方法,返回一个BigInteger随机素数; BigInteger(int , int , Random) //更好的表达方式 public static BigInteger probablePrime();
一个类特定签名格式的构造函数是唯一的,程序员围绕这个限制提供了两个构造函数,他们区分的标准是不一样参数的类型组成的参数列表,这真的是一个糟糕的设计,使用这种API的人从来就没办法记住哪个构造函数对应哪个功能,并且容易误用导致调用错误;使用这些构造函数的人只能阅读代码,类文档基本不会提到。因为静态工厂方法有名字,所以不存在区分的限制,在一个类有多个签名的构造函数的场景中,应该使用静态工厂方法替代构造函数,并且小心选择名字来区别;
优点2:不需要每次被调用的时候都创建一个新的对象;
这允许构造完毕的不可变类使用提前构建或者缓存的实例,避免创建不必要的重复的对象(分配重复的实例); Boolean.valueOf(boolean) 方法展示了这个技巧,它从来不创建对象,这个技巧类似于享元模式;如果创建代价昂贵的对象被频繁获取,它可以显著的提高性能;这个能力可以让类在任何时刻严格控制实例,这样的类叫做实例可控类,有几个理由应该写实例可控类:
1,实例可控使类需要保证它是单例的实例数量可控;
2,需要保证没有两个相同的实例存在;如果a.equls(b) ; 有且只有a == b. 这是基本的享元模式。而枚举类型提供了这个保证;
优点3:可以返回子类型,面向的抽象编程或者说是接口编程,更灵活;
一个灵活的应用API可以返回非公开的对象,隐藏实现类可以形成契约的API,这个技巧使得它自己成为基于接口的框架,接口提供自然的返回类型,(在java8之前,接口没有静态的方法,按照惯例,接口Type静态工厂方法被放到了一个不可实例化的指南类Types中;) 举个例子:java的Collections接口框架有45个种不同的应用实现,提供不可修改集合,同步集合等,几乎所有的这些实现通过一个不可实例化的类Collections的静态工厂方法来对外暴露;所有的返回对象都是非公开的的(即返回的是接口实现类或者返回类的子类)。这个集合框架的API比定义45个分开独立的公共类规模要小很多(它减少了集合类的数量负担,也减少了程序员必须掌握使用的API的类数量),程序员知道返回的对象精确的指出了它的接口,所以没有必要去阅读更多的实现类的文档。更进一步,使用这样一个静态工厂方法的客户端只需要参照接口而不用考虑实现类,这是一个最佳实践。
在java8中,接口已经可以添加静态方法了,所以接口可以提供一个不可实例的有常规名字的指南类;接口的很多的公共静态成员放应该替代成接口本身。然而,仍然有必要把这样一个静态工厂方法实现代码放到分开的private 包下的类中;这是因为java8要求接口的静态成员必须是public,java9允许私有的静态方法,但是静态成员和静态内部类任然需要是public的;
优点4:返回对象随输入参数而变化,更灵活;
申明的返回类型的任何子类型是允许返回的,返回类型可以随版本变化。举个例子:EnumSet这个类没有公共的构造函数,只有静态工厂方法(EnumSet.of(E e1, E... rest)),在openJDK的实现中,它的返回依赖放入的枚举的数量。如果元素数量小于64,静态工厂返回一个RegularEnumSet的实例;如果元素大于64,返回一个通过long数组备份JumboEnumSet的实例;
这两个实现类的存在对客户端来说不可见的,选择RegularEnumSet来返回的情况适合枚举数量较少的,这在未来的版本中不会有副作用,类似的,为了提供更好的性能,未来的版本可以添加第三种或者第四种EnumSet的子类实现,客户端不需要关心从该工厂方法中返回的实例,它们只需要关心他是EnumSet的子类;
优点5:返回类型的实例可以随客户端的实现而定制或者说来桥接;
这个灵活的工厂方法就是一个基本的服务提供框架。类似于JDBC的API。
服务提供框架是一个提供服务实现的系统,实现是依据客户端来的,所以解耦了客户端的实现。
一个服务提供框架有三个基本的组件:
服务接口 Service Interface : 代表一个功能抽象
服务提供者注册API Service Provider Registration API : 实现提供的服务和服务的注册
服务访问API Service Access API :客户端使用它来获取服务接口的实例
服务访问API允许客户端通过指定的条件来选择一个实现,缺省的条件下,返回一个默认的实现,或者允许客户端循环使用可用的服务接口实现。
服务访问API是基本的服务提供框架的一种灵活的静态工厂方法形式。
服务提供框架可选的第四个组件是:服务提供接口 Service Provider Interface; 描述了提供服务接口实现的工厂实例,缺省的服务提供接口,它的实现必须通过反射来实例化服务接口实例。
在jdbc的例子中,Connection 扮演服务接口的角色 , DriverManager.registerDriver服务提供者注册API , DriverManager.getConnection是服务访问API, Driver是服务提供接口;
有很多的服务提供框架模式。比如:服务访问API可以返回更丰富的服务接口到客户端,而不是它自己提供的,这是桥接器模式。
依赖注入框架可以被看做强有力的服务提供者,从java6开始, 平台提供了一个常规的服务提供框架, java.util.ServiceLoader , 你没有必要写你自己的。jdbc没有使用ServiceLoader , 因为jdbc比ServiceLoader更早;
限制1:静态工厂方法返回的类如果没有公共的或者受保护构造方法的类它是不能有子类,灵活性不具备;
举个例子:你无法子类化集合框架返回的任何内部实现类,比如
UnmodifiableCollection
,这是一件好事,因为它鼓励程序员去使用组合而不是继承,并且需要不可变的类型;限制2:静态工厂方法创建实例很难被程序员发现;
它不像构造函数一样出现在API文档中,所以它很难被找到如何通过静态工厂方法而不是通过构造函数去创建实例;java文档有一天也许会关注到静态工厂方法。有一些静态工厂方法的常规名字。但是这个列表远不够丰富;
1, Date.from(instant)
2, EnumSet.of(jack)
3, BigInterger.valueOf(100)
4, StackWalker.getInstance(xx)
5,, Array.newInstance(xxx)
6, Files.getFieStore(path)
7,Files.newBufferedReader(path)
8,Collections.list(xxx)
总结:通过静态工厂方法和公共构造函数来创建实例有他们自己的适合的使用场景,值得去理解他们的相对优点,通常静态工厂方法是优先选择的,所以,优先考虑静态工厂方法来创建实例,尽量避免或者少用反射调用公共构造函数来创建实例;
使用静态工厂方法去创建实例有明显的优势。
1,可读性好 ;
2,不需要每次都创建新的实例,内存效率高 ;
3,返回类型可以是子类型,也可以接口实现类,或者是一个服务提供框架SPI配置的接口,增加了编程的灵活性和扩展性
但是静态工厂方法也有明显的使用限制:
1,不太好找出来,只能通过通用的方法名去找;
2,无法子类化构造函数不是public或者protect的返回类,这个时候缺少灵活性;
里面提到了一个服务提供框架的基本组成,和一个jdk自带的依赖注入框架 ServiceLoader;
原创不易,转载请注明出处,一起学习Effective java 3,提高代码质量,编程技能。欢迎一起讨论。