当单例模式碰上序列化
- 一、问题如下
- 二、解决办法
- 三、原理剖析
- 3.1 对象反序列化
- 3.2 枚举反序列化
问题如下
单例模式:
public class MmSingleton implements Serializable {
private MmSingleton() {
if (INSTANCE != null) {
//预防AccessibleObject.setAccessible通过反射机制调用私有构造器
throw new RuntimeException("创建对象失败!");
}
}
private static final MmSingleton INSTANCE = new MmSingleton();
public static MmSingleton getInstance() {
return INSTANCE;
}
private Object readResolve() {
return INSTANCE;
}
}
客户端调用:
MmSingleton mm1 = MmSingleton.getInstance();
MmSingleton mm2 = null;
//序列化mm1
FileOutputStream fos = new FileOutputStream("mm.java");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(mm1);
//将内容反序列化到mm2
FileInputStream fis = new FileInputStream("mm.java");
ObjectInputStream ois = new ObjectInputStream(fis);
mm2 = (MmSingleton) ois.readObject();
System.out.println(mm1 == mm2);
客户端调用返回false。
解决办法
Effective Java中推荐的解决办法如下:
将Singleton类变成是可序列化的,仅仅在声明中加上implements Serializable是不够的。为了维护并保证Singleton,必须声明所有实例域都是瞬时(transient)的,并提供一个readResolve方法。否则每次反序列化都会创建一个新的实例。
在MmSingleton类中提供方法:
private Object readResolve() {
return INSTANCE;
}
再次使用客户端调用,得到结果为true。
Effective Java中还提到,使用枚举实现Singleton为最佳方法。
public enum MmEnumSingleton {
INSTANCE;
}
如果Singleton必须扩展一个超类,而不是扩展Enum的时候,则不适宜使用枚举。
原理剖析
重点在于反序列化时,为啥对应的引用会因为上述而改变?我们查看源码ois.readObject();
public final Object readObject()
throws IOException, ClassNotFoundException
{
//...省略部分源码
Object obj = readObject0(false);
//...省略部分源码
}
找到readObject0方法,进入该方法,可发现switch语句对Object类和枚举都有不同的序列化方法,如下:
private Object readObject0(boolean unshared) throws IOException {
//...省略部分源码
switch (tc) {
case TC_ENUM:
return checkResolve(readEnum(unshared));
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
}
//...省略部分源码
}
对象反序列化
查看readOrdinaryObject方法:
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
//...省略部分源码
//判断desc是否可实例化,可以则通过反射调用无参构造方法
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
//...省略部分源码
//obj不为空且有readResolve方法
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
通过上述代码可知,调用被反序列化类中的readResolve方法
Object rep = desc.invokeReadResolve(obj);
而该方法又返回同一个实例:
private Object readResolve() {
return INSTANCE;
}
最终赋值给obj并返回。
枚举反序列化
参考:https://docs.oracle.com/javase/7/docs/platform/serialization/spec/serial-arch.html#6469
枚举常量的序列化形式仅由其名称组成,而反序列化时通过调用java.lang.Enum.valueOf方法,将常量的枚举类型与接收到的常量名称作为参数传递,来获得反序列化的常量。
任何类特异性 writeObject,readObject, readObjectNoData,writeReplace和 readResolve由枚举类型定义的方法被序列化和反序列化期间被忽略。
Enum.java类源码如下:
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}