文章作者:Tyan
博客:noahsnail.com | CSDN | 简书
Item 3 用私有构造函数或枚举类型强化单例属性
单例简单来说就是一个类只被实例化一次[Gamma95, p. 127]。通常单例表示一个系统组件在本质上来说是唯一的,例如窗口管理或文件系统。一个类成为单例会使它的客户端测试变得很困难,因为不可能用伪实现来代替单例,除非它实现了一个接口,这个接口作为它的服务类型。
在1.5版本之前,有两种方式来实现单例。它们都是通过保持私有构造函数并输出一个公有静态成员来提供对类唯一实例的访问来实现的。在第一种方法中,公有静态成员被声明为final字段:
1 | // Singleton with public final field |
为了初始化公有静态final变量Elvis.INSTANCE
,私有构造函数只调用一次。公有或保护构造函数的缺失保证了全局唯一性:确切的说一旦Elvis
类初始化,将只有一个Elvis
实例存在——不会多也不会少。客户端不能改变这个情况,但要提醒一点:有特权的客户端可以借用AccessibleObject.setAccessible
方法方法,通过反射机制(Item 53)的调用私有构造函数。如果你需要抵御这种攻击,修改构造函数使它在创建第二个实例时抛出异常。
1 | // Singleton with static factory |
所有Elvis.getInstance
方法的调用都会返回同一个对象实例,并且不会有其它的Elvis
实例被创建(提醒同上)。
公有变量方法的主要优势在于更清晰的声明这个类是一个单例类:公有静态变量是final的,因此它总是包含同一个对象的引用。公有变量方法没有任何性能优势:现代Java虚拟机(JVM)的大多数实现都是将静态工厂方法当做内联函数来调用。
为了使上面方法实现的单例类可序列化(第11章),仅仅在它的声明中实现Serializable
接口是不够的。为了保证单例性,你必须将所有的实例变量声明为transient
并提供一个readResolve
方法(Item 77)。否则,每次一个序列化的实例在反序列化时将会创建一个新的实例,在我们的例子中,会看到一个假的Elvis
。为了防止这种情况发生,要在Elvis
类中添加readResolve
方法:
1 | // readResolve method to preserve singleton property |
在1.5版本中,有第三种实现单例的方法。简单声明一个只有一个元素的枚举类型:
1 | // Enum singleton - the preferred approach |
这个方法除了它更简洁之外,它在功能上等价于公有变量方法,免费提供了序列化机制,并且强有力的保证了不会被多次实例化,即使是在面临复杂的序列化或反射攻击时。虽然这个方法仍没有被广泛采用,但单元素的枚举类型是实现单例的最好方式。