ITEM 3: ENFORCE THE SINGLETON PROPERTY WITH A PRIVATE CONSTRUCTOR OR AN ENUM TYPE
单例是一种只能实例化一次的类。单例通常表示无状态对象,如函数或系统中的唯一组件。单例的类会使用户代码的测试其变得困难,因为除非它实现了作为其类型的接口,否则就不可能用模拟实现代替单例。
实现单例有两种常见的方法,它们都基于保持构造函数私有和导出公共静态成员来提供对惟一实例的访问。在一种方法中,成员是一个公共的、final的字段:
// Singleton with public final field
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}
私有构造函数只被调用一次,用于初始化成员变量 INSTANCE。没有public / protected 构造函数,这样能保证全局仅存在一个Elvis 实例。但需要注意的是,特权用户可以在 AccessibleObject.setAccessible() 方法的帮助下通过反射调用私有构造函数。如果想填补这个漏洞,我们可以修改私有构造函数,当用户试图创建第二个实例时抛出异常。
第二种方法是利用一个静态工厂方法返回唯一实例:
// Singleton with static factory
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public static Elvis getInstance() { return INSTANCE; }
public void leaveTheBuilding() { ... }
}
所有对 Elvis.getInstance() 方法的调用都会返回同一个实例,不会创建其他新的实例(同样存在上一个方法的特权用户问题)。
公共字段方法的主要优点是,API明确表示该类是单例的:公共静态字段是final,因此它总是包含相同的对象引用;而且它更简单。
静态工厂方法的一个优点是,我们可以灵活地改变想法,不论类是否为单例,我们不需要改变API。工厂方法返回唯一的实例,但是可以修改为,例如返回调用它的每个线程的单独实例。第二个好处是,如果有需要,我们可以写一个泛型单例工厂。A final advantage of using a static factory is that a method reference can be used as a supplier, for example Elvis::instance is a Supplier<Elvis>. (???)。除非有以上这些好处,否则更推荐公共字段的方案。
如果需要使单例类可序列化,仅仅实现 Serializable 接口是不够的。为了维护单例保证,需要声明所有实例字段为transient,并提供一个readResolve方法。否则,每次反序列化发生时,都会创建一个新实例。以上面的类 Elvis 为例,会导致创建新的 Elvis 实例,为了防止出现这种情况,需要实现 readResolve 方法:
// readResolve method to preserve singleton property
private Object readResolve() {
// Return the one true Elvis and let the garbage collector
// take care of the Elvis impersonator.
return INSTANCE;
}
第三种方法是使用枚举:
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}
这种方法类似于公共字段方法,但是它更简洁,天然提供序列化,并且提供了针对多个实例化的可靠保证,即使面对复杂的序列化或反射攻击也是如此。这种方法可能有点不自然,但是单元素枚举类型通常是实现单元素的最佳方法。注意,如果单例必须扩展枚举之外的超类,则不能使用此方法(尽管可以声明枚举来实现接口)。
网友评论