条目3:利用私有构造器或枚举类型强化Singleton属性
Singleton是什么?
是指只能实例化一次的类。Singleton通常用于表示无状态的对象,函数,或本质上唯一的系统组件。将一个类设计为Singleton会使其客户端测试变得十分困难,因为Singleton不能被继承,我们无法创建一个用来代替它的模拟实现。
常见实现Singleton的方式
-
使用final字段作为这个公有静态成员
public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elvis(){ ... } public void leaveTheBuilding() {...} }
-
使用静态工厂作为公有的成员
public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elvis(){ ... } public static Elvis getInstance() {return INSTANCE;} public void leaveTheBuilding() {...} }
这两种方法的原理都是将构造器设置为私有,通过到处一个公有的静态成员来提供唯一实例。
第一种方式的优点:
- API可以清楚的看到该类是一个Singleton
第二种方式的优点:
- 更简单
- 提供了灵活性,便于扩展和修改
这两种方法都会有一个问题,对于越权的客户端都可以借助AccessibleObject.setAccessible
方法,通过反射机制调用私有构造器,如果想防御这类攻击,可以修改构造器,使其被要求创建第二个实例时抛出异常。
除此之外,想让他们支持序列化,仅靠在声明中添加implements Serializable
时不够的,为了保证其Singleton的性质,需用transient
来声明其所有实例对象,并提供一个readResolve
方法。
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class SerializableSingleton implements Serializable {
// 私有静态实例
private static final SerializableSingleton INSTANCE = new SerializableSingleton();
// 私有构造方法,防止外部实例化
private SerializableSingleton() {
if (INSTANCE != null) {
throw new IllegalStateException("Singleton instance already created!");
}
}
// 提供公有的获取实例方法
public static SerializableSingleton getInstance() {
return INSTANCE;
}
// readResolve 方法保证反序列化返回同一个实例
private Object readResolve() {
return INSTANCE;
}
// 示例方法
public void showMessage() {
System.out.println("Hello from Serializable Singleton!");
}
}
添加
readResolve
方法后,一个类可以在反序列化时控制返回的对象,从而保证序列化支持和其他约束(如Singleton的唯一性)。这与Java对象序列化机制的工作原理密切相关。
Java序列化的机制
Java序列化使用
java.io.Serializable
接口,涉及两个过程:序列化
- 使用
ObjectOutputStream
将对象的状态写入字节流。- JVM保存类的元数据(字段、类型等)和实例数据(字段值)。
反序列化
使用
ObjectInputStream
从字节流中读取对象。JVM通过反射重新创建对象,绕过构造器,直接分配内存并填充字段值。
问题:为什么反序列化破坏了Singleton?
在反序列化过程中:
- JVM默认创建一个全新的对象,即使该类已经是Singleton。
- 此新对象是通过直接分配内存的方式构造,而不是通过原有的
getInstance
方法。- 因此,反序列化会生成一个与原Singleton实例不相等的新实例,违背了Singleton模式的唯一性原则。
readResolve
的作用在反序列化完成后,JVM会调用
readResolve
方法(如果定义了该方法),并将其返回值作为反序列化的最终结果。执行流程
反序列化后,JVM会创建一个对象,并尝试用字节流中的数据恢复其状态。
在对象恢复完成后,
ObjectInputStream
会检查该对象是否定义了readResolve
方法。如果定义了
readResolve
,JVM会用该方法的返回值替代反序列化生成的新对象。
java.io.ObjectInputStream
源码
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
...
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod()) // 检查是否有readResolve方法
{
Object rep = desc.invokeReadResolve(obj); // 调用readResolve方法
...
}
return obj;
}
-
声明一个只包含单个元素的枚举类型(首选方法)
public enum Singleton { INSTANCE; // 唯一实例 // 示例方法 public void doSomething() { System.out.println("Singleton instance is working!"); } }
public class SingletonTest { public static void main(String[] args) { Singleton instance1 = Singleton.INSTANCE; Singleton instance2 = Singleton.INSTANCE; // 验证是否是同一个实例 System.out.println(instance1 == instance2); // 输出 true // 调用方法 instance1.doSomething(); } }
优点:
- 简洁
- 自带序列化机制
- 防止多次实例化
注意:
如果你的
Singleton
类需要继承一个特定的父类(超类),那么你无法通过枚举来实现 Singleton 模式,因为 Java 的枚举类型不能继承任何自定义的类。public enum Singleton extends SomeClass { // 编译错误 INSTANCE; }
枚举类隐式会继承
java.lang.Enum
Java由是一个单继承语言。因此需要这种设计的时候就不可以使用该方法。