天天看点

当单例模式碰上序列化当单例模式碰上序列化

当单例模式碰上序列化

  • 一、问题如下
  • 二、解决办法
  • 三、原理剖析
  •   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);
}