Effective Java 3rd 条目1 考虑用静态工厂方法代替构造子

对于一个类,让客户端获取一个实例的传统方式是,提供一个公开的构造子(constructor)。另外一个技巧,应该成为每个程序员的工具库之一。一个类可以提供一个公开的静态工厂方法(static factory method), 这个静态方法简单地返回这个类的实例。Boolean是boolean 的原始装箱类(boxed primitive),这里有关于Boolean的简单例子。下面方法把boolean原始值转换到Boolean对象引用:

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

注意,这个静态工厂方法和设计模式 [Gamma95]的工厂方法是不一样的。这个条目描述的静态工厂方法在设计模式中没有直接的对映物。
一个类可以通过静态工厂方法,而不是或者除了通过公开的构造子,提供给它的客户端。提供静态工厂方法而不是公开的构造子同时有优缺点。

静态工厂方法的一个优点是,不像构造子,它有名字

构造子的参数本身,不是用来描述返回对象的。一个有适当名字的静态工厂更容易使用,而且在客户端的代码也容易读懂。比如,构造子BigInteger(int, int, Random),返回了一个有可能是质数的BigInteger,这个能更好的表达为一个静态工厂方法Integer.probablePrime(这个方法在Java 4加入了)。

对于一个类来说,一个给定的签名,仅仅只能有一个构造子。程序员用一种方法绕过这个限制:提供两个构造子,它们的参数列表仅仅在参数类型的顺序方面不同。这确实是一个糟糕的主意。这样的API的使用者不能够记得哪个构造子是哪个,有可能无意使用了错误的构造子。如果没有参考类的文档,读这些调用构造子的代码,人们不知道这些代码是干什么的。

因为静态工厂方法有名字,它没有上面讨论的限制。当类需要相同签名的多个构造子,用静态工厂方法替代构造子,这些方法需要细心选取的名字来突出他们的区别。

静态工厂方法的第二个优点是,不像构造子,它们被调用的时候,不必要每次都创建一个新的对象

这允许不变类使用预先构造的实例,或者当他们构造时缓存的实例,不停地调用而不需要创建没必要的重复的对象。Boolean.valueOf(boolean)方法诠释了这个技巧:它从不创建一个对象。这个技巧和享元模式[Gamma95]是相似的。如果等价的对象被经常请求,特别是这些对象创建起来消耗很大,这可以极大的提高性能。

静态工厂方法可以从不断调用过程中返回同一个对象,这可以让类在任何时间来维护对什么实例可以存在的严格控制。这样的类被叫做控制实例的(instance-controlled)。有不同的原因写控制实例的类。控制实例让一个类保证它是单例(singleton)(条目3)或者是不可实例化的(noninstantiable(条目4)。而且,这让一个值不变的类(条目17)可以保证没有两个相等的实例存在:当且仅当a==b时a.equals(b)。这是享元模式的基础。枚举类型(条目34)提供了这种保证。

静态工厂方法的第三个优点是,不像构造子,它们可以返回它们返回类型的子类对象。这可以在你选取返回对象的类型提供了很大的灵活性

这个灵活性的一个应用是,一个API可以返回一个对象,而这个对象的类不公开。用这种方式来隐藏实现类,这样实现了一个非常简约的API。这个技巧直接导致了一个基于接口的框架(interface-based frameworks)(条目20),接口为静态工厂方法提供了自然的返回类型。

在Java 8之前,接口是不能有静态的方法。按照惯例,名字为Type的接口的静态工厂方法放在名字为Types的不可实例化的companian类中(条目4)。比如,Java Collections Framework有基于它的接口的45个效用(utility)实现,提供了不可改变的集合,同步的集合等等。几乎所有的这些实现是在一个不可实例化的类(java.util.Collections)中通过静态工厂方法导出的。返回的对象的类都是非公开的。

假设分别导出45个公开类,每个公开类有对应的便捷的实现,Collections Framework API就比这样小很多。这不仅仅减少了API的批量,而且减少了概念权重(conceptual weight):程序员为了使用API必须掌握概念的数量和难度。程序员知道返回对象正好有接口指定的API。所以,就没必要额外读实现类的文档。进一步地,用这样的静态工厂方法需要客户端用接口来引用返回的类型,而不是实现类,这通常是一个最佳实践(条目64)。

在Java 8中,接口不能包含静态方法这个限制被移除了,所以,通常没有必要为接口提供一个不能实现的companian类。原先在这样的类中的许多公开静态成员,应该放在接口本身中。然后,注意,把这些静态方法背后的实现代码片段放到一个独立的包私有的类中,这还是有必要的。这是因为在Java 8要求接口的所有静态方法都必须是公开的。Java 9允许私有的静态方法,但是静态成员类还是要求公开的。

静态工厂的第四个优点是,返回对象的类可以随着调用输入参数的函数改变而改变。声明的返回类型的子类型是允许的。返回对象的类也是随着发布而改变的

EnumSet类(条目36)没有公开的构造子,仅仅有静态工厂。在OpenJDK的实现中,返回两个子类之一的实例,这取决于下面的枚举类型:如果有64个或者更少的元素(大多数枚举类型都是),静态工厂返回由单个long支持的RegularEnumSet实例;如果枚举类型有65个或者更多的元素,工厂返回由long队列支持的JumboEnumSet实例。

这两个实现的类的存在对客户端来说是不可见的。如果RegularEnumSet没有对小枚举类型的性能优势,在未来发布的版本可以被移除掉而没有不好的影响。类似的,未来的发布可以添加第三个第四个EnumSet的实现,如果它被证明对性能有利。客户端即没必要知道,也不关心他们从工厂取得的对象的类;他们仅仅关心它是EnumSet的子类。

静态工厂的第五个优点是,当包含方法的类已经写了,返回对象的类没必要存在

这个灵活的静态工厂方法形成了服务提供框架(service provider frameworks)的基础,就像Java Database Connectivity API(JDBC)。一个服务提供框架是一个提供者实现了一个服务的系统,这个系统使得客户端可以使用这些实现,把客户端从实现中解耦了。

一个服务提供框架有三个必要的组件:表示实现的服务接口(service interface)、提供用户注册实现的提供者注册API(provider registration API)和客户端用来获取服务实例的服务获取API(service access API)。服务获取API可以允许客户端指定选择一个实现的规则。缺少了这样的规则,API将返回一个默认的实现,或者让客户端循环所有找到的实现。服务获取API是形成了服务提供者基础的灵活静态工厂。

服务提供者框架可选的第四个组件是服务提供者接口,描述了一个生成服务接口实例的工厂对象。没有服务提供者接口,实现必须反射实例化(条目65)。JDBC来说,Connection是服务接口的角色,DriverManager.registerDriver是提供者注册API,DriverManager.getConnection是服务获取API,Driver是服务提供者接口。

服务提供者框架模式有许多变体。比如,服务获取API,相对于提供者修饰的,可以返回更加丰富的服务接口。这叫桥接模式[Gamma95]。依赖注射框架(条目5)可以看成是强大的服务提供者。自从Java 6,平台包含了一个通用目的的服务提供者框架,java.util.ServiceLoader,所以没必要,通常也不应该写自己的实现(条目59)。JDBC没有用ServiceLoader,因为前者早于后者。

仅仅提供静态工厂方法的主要限制在没有公开的或者受保护的构造子不能够有子类

比如,Collections Framework中任何便捷实现类没有子类。可以认为,这是看似坏事的好事,因为这鼓励程序员使用组合而不是继承(条目18),这是不可变类型要求的。

静态工厂方法的第二个缺点是,程序员难于发现这些方法

在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 或者 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);

总结,静态工厂方法和公开的构造子都有他们的用处,理解他们的相对优点是值得的。静态工厂常常是更可取的,所以避免第一反应:没有首先考虑静态工厂而提供公开构造子。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值