effective java
1.用静态工厂方法代替构造器
对于类而言,为了让客户端获取它自身的一个实例,最传统的方法就是提供一个公有 的构造器。 还有一种方法,也应该在每个程序员的工具箱中占有一席之地。 类可以提供一个 公有的静态工厂 方法( static factory method),它只是一个返回类的实例的静态方法。 下面是 一个来自 Boolean (基本类型 boolean 的装箱类)的简单示例。 这个方法将 boolean 基本 类型值转换成了一个 Boolean 对象引用:
public static Boolean valueOf(boolean b) {
return b? Boolean.TRUE : Boolean.FALSE;
}
注意,静态工厂方法与设计模式[ Gamma95 ]中的工厂方法( Factory Method) 模式不 同。 本条目中所指的静态工厂方法并不直接对应于设计模式(Design Pattern)中的工厂方法。 如果不通过公有的构造器, 或者说除了公有的构造器之外,类还可以给它的客户端提 供静态工厂方法。 提供静态工厂方法而不是公有的构造器,这样做既有优势,也有劣势。
1.1 静态工厂的优势
1.1.1 静态工厂方法与构造器不同的第一大优势在于,它们有名称。
如果构造器的参数本 身没有确切地描述正被返回的对象,那么具有适当名称的静态工厂会更容易使用,产生的 客户端代码也更易于阅读。 例如,构造器 Biginteger (int, int, Random)返回的 Biginteger 可能为素数,如果用名为 Biginteger.probablePrime 的静态工厂方法 来表示,显然更为清楚。 (Java 4 版本中增加了这个方法。)
一个类只能有一个带有指定签名的构造器。 编程人员通常知道如何避开这一限制 : 通过提供两个构造器,它们的参数列表只在参数类型的顺序上有所不同。 实际上这并不是个好主 意。面对这样的 API, 用户永远也记不住该用哪个构造器, 结果常常会调用错误的构造器。 并且在读到使用了这些构造器的代码时,如果没有参考类的文档,往往不知所云。
由于静态工厂方法有名称,所以它们不受上述限制。 当一个类需要多个带有相同签名 的构造器时,就用静态工厂方法代替构造器,并且仔细地选择名称以便突出静态工厂方法之 间的区别。
类的构造器同一签名的情况下只能有一个,比如
public class Desk { private String color; private String height; private String width; //这两个构造器同时只能存在一个 public Desk(String color) { this.color = color; } //上面有了下面的就会编译错误 public Desk(String width){ this.width = width; } public Desk(String color, String height, String width) { this.color = color; this.height = height; this.width = width; } }
通过静态工厂方法返回对象,可以避免这种问题,并且通过良好的方法命名,静态工厂方法使用起来更加见名知意。 (原谅名字确实起的很烂)
public class Desk { private String color; private String height; private String width; //构造器私有化 private Desk(String color, String height, String width) { this.color = color; this.height = height; this.width = width; } //获得一个有颜色的桌子 public static Desk colorDesk(String color){ return new Desk(color, null, null); } //获得一个有宽度的桌子 public static Desk widthDesk(String width){ return new Desk(null, null, width); } }
1.1.2 静态工厂方法与构造器不同的第二大优势在于,不必在每次调用它们的时候都创建一个新对象。
这使得不可变类(详见第 17 条)可以使用预先构建好的实例,或者将 构建好的实例缓存起来, 进行重复利用,从而避免创建不必要的重复对象。 Boolean. valueOf (boolean )方法说明了这项技术 : 它从来不创建对象。 这种方法类似于享元 (Flyweight)模式[ Gamma95 ] 。 如果程序经常请求创建相同的对象,并且创建对象的代价 很高,则这项技术可以极大地提升性能。
静态工厂方法能够为重复的调用返回相同对象,这样有助于类总能严格控制在某个时刻哪些实例应该存在。 这种类被称作实例受控的类( instance-controlled) 。 编写实例受控的 类有几个原因 。 实例受控使得类可以确保它是一个 Singleton (详见第 3 条)或者是不可实例 化的(详见第 4 条)。 它还使得不可变的值类(详见第 17 条)可以确保不会存在两个相等的 实例, 即当且仅当 a==b 时, a . equals(b )才为 true。 这是享元模式[ Gamma95 ] 的基 础。 枚举(enum)类型(详见第 34 条)保证了这一点。
这个就不多解释了,参考单例模式,利用枚举类实现单例。
1.1.3 静态工厂方法与构造器不同的第三大优势在于,它们可以返回原返回类型的任何子类型的对象
这样我们在选择返回对象的类时就有了更大的灵活性。 这种灵活性的一种应用是, API 可以返回对象,同时又不会使对象的类变成公有的。以 这种方式隐藏实现类会使 API 变得非常简洁。 这项技术适用于基于接口的框架( interface-based framework) (详见第 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 中允许接口有私有的静态方法,但是静态域和静态成员类仍然需要是公 有的。
合理使用这种方式,能够实现一些很不错的写法。创建对象时通过不同的参数,获取父类引用子类实现。
感觉配合枚举更合适。
/** * 检测的接口 * * @author Xue-Pan * @date 2020/2/8 */ public interface ValidateHandler { //用于检测的方法 String valid(String info); } /** * 效验的父类 * * @author Xue-Pan * @date 2020/2/8 */ public class FatherValidateHandler implements ValidateHandler{ //因为要被继承所以不能私有化,这是静态工厂方法的劣势之一,不过可以用复合来解决。 protected FatherValidateHandler(){ } //不同的类型获取不同的实现子类,适用性好很多。 public static FatherValidateHandler createValidateHandler(String type){ if("phone".equals(type)){ return new PhoneValidateHandler(); }else if("card".equals(type)){ return new CardValidateHandler(); }else{ return null; } } @Override public String valid(String info) { return "这是父类的实现"; } //测试的方法 public static void main(String[] args) { FatherValidateHandler card = FatherValidateHandler.createValidateHandler("card"); String v = card.valid("111"); System.out.println(v); } } public class CardValidateHandler extends FatherValidateHandler { @Override public String valid(String info) { return "通过卡来认证"; } } public class PhoneValidateHandler extends FatherValidateHandler { @Override public String valid(String info) { return "获取到了手机号,进行了认证"; } }
1.1.4 静态工厂的第四大优势在于,所返回的对象的类可以随着每次调用而发生变化,这取 决于静态工厂方法的参数值。
只要是已声明的返回类型的子类型,都是允许的。 返回对象的 类也可能随着发行版本的不同而不同。EnumSet (详见第 36 条)没有公有的构造器,只有静态工厂方法。 在 OpenJDK 实现中, 它们返回两种子类之一的一个实例,具体则取决于底层枚举类型的大小:如果它的元素有 64 个或者更少,就像大多数枚举类型一样,静态工厂方法就会返回一个 RegalarEumSet 实例, 用单个 long 进行支持;如果枚举类型有 65 个或者更多元素,工厂就返回 JumboEnumSet 实例,用一个 long 数组进行支持。 这两个实现类的存在对于客户端来说是不可见的。 如果 RegularEnumSet 不能再给 小的枚举类型提供性能优势,就可能从未来的发行版本中将它删除,不会造成任何负面的影 H向。 同样地,如果事实证明对性能有好处,也可能在未来的发行版本中添加第三甚至第四个 EnumSet 实现。 客户端永远不知道也不关心它们从工厂方法中得到的对象的类,它们只关 心它是 EnumSet 的某个子类。
这一点跟第三点有点接近。相当于对参数进行判断来返回不同的实现类。
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) { Enum<?>[] universe = getUniverse(elementType); if (universe == null) throw new ClassCastException(elementType + " not an enum"); if (universe.length <= 64) return new RegularEnumSet<>(elementType, universe); else return new JumboEnumSet<>(elementType, universe); }
1.1.5 静态工厂的第五大优势在于,方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在。
这种灵活的静态工厂方法构成了服务提供者框架( Service Provider Framework)的基础,例如 JDBC(Java 数据库连接)API。 服务提供者框架是指这样一个系统:多个服务提供者实现一个服务,系统为服务提供者的客户端提供多个实现,并把它们从多个实现中解耦出来。
服务提供者框架中有三个重要的组件:服务接口 ( 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)模式 [ Gamma95 ] 。 依赖、注入框架(详见第 5 条) 可以被看作是一个强大的服务提供者。 从 Java 6 版本开始, Java 平台就提供了一个通用的 服务提供者框架 java . util . ServiceLoader ,因此你不需要(一般来说也不应该)再自己编写了(详见第 59 条)。 JDBC 不用 ServiceLoader ,因为前者出现得比后者早。
这是指通过静态工厂获取对象,然后用提供的接口来进行接收。实际上获取的对象是由提供者来实现的。
感觉在spring的环境下,更易于使用。比如:我会从容器中获取到A,然后调用A的do方法,A是一个接口。你写了一个实现了A的B接口,然后创建对象注入容器。结果就是我用你的对象调用了do方法。如果有变化,你可以重新注入一个实现了A的C对象,而我这里就变成了调用C的do方法。
1.2静态工厂的劣势
1.2.1 静态工厂方法的主要缺点在子,类如果不含公有的或者受保护的构造器,就不能被子类化。
例如,要想将 Collections Framework 中的任何便利的实现类子类化, 这是不可能的。 但是这样也许会因祸得福,因为它鼓励程序员使用复合( composition),而不是继承(详见 第四条),这正是不可变类型所需要的(详见第 17 条)。
构造器私有化的类无法被继承,而通过静态工厂方法返回实例的对象,最好是将构造器私有化,防止通过构造器创建对象,但这样一来,该类便无法被继承。但可以通过复合来实现继承的效果(策略模式),即不使用继承的 是什么概念,而是使用 能干什么的感念。
1.2.2 静态工厂方法的第二个缺点在于,程序员很难发现它们。
在 API 文档中,它们没有像 构造器那样在 API 文档中明确标识出来, 因此 对于提供了静态工厂方法而不是构造器的 类来说,要想查明如何实例化一个类是非常困难的。 Javadoc 工具总有一天会注意到静态工 厂方法。 同时,通过在类或者接口注释中关注静态工厂, 并遵守标准的命名习惯,也可以弥 补这一劣势。 下面是静态工厂方法的一些惯用名称。 这里只列出了其中的一小部分:
- from一一类型转换方法,它只有单个参数,返回该类型的一个相对应的实例,例如:
Dated= Date.from(instant) ;
- of 聚合方法,带有多个参数,返回该类型的一个实例,把它们合并起来,例如 :
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 一样,但 create 或者 newInstance 能够确保每次调用都返回一个新的实例 ,例如:
Object newArray = Array.newInstance(classObject, arrayLen);
get Type一一像 getInstance 一样,但是在工厂方法处于不同的类中的时候使用。Type 表示工厂方法所返回的对象类型,例如:
FileStore fs = Files.getFileStore(path);
newType一一像newInstance 一样,但是在工厂方法处于不同的类中的时候使用。Type 表示工厂方法所返回的对象类型,例如:
BufferedReader br= Files.newBufferedReader(path);
type一一getType 和 newType 的简版,例如:
List<Complaint> litany = Collections.list(legacylitany);
简而言之,静态工厂方法和公有构造器都各有用处,我们需要理解它们各自的长处。 静态工厂经常更加合适,因此切忌第一反应就是提供公有的构造器, 而不先考虑静态工厂。
文档信息
- 本文作者:chayedankase
- 本文链接:https://chayedankase.github.io/2020/02/08/effective-java1/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)