设计模式 | 挑战单例模式(七)

275 阅读2分钟

这是我参与8月更文挑战的第30天,活动详情查看:8月更文挑战

内容接着上篇文章《挑战单例模式(六)》,上篇文章讲到了序列化会破坏单例,这篇文章来讲如何避免反射和序列化破坏单例实现。

九、注册式单例

上面说到,反射和序列化都会破坏单例的实现,注册式单例又被称为登记式单例,就是将每个实例都登记到某个地方,使用唯一的标识获取单例。注册单例有两种实现方式。

  • 容器缓存
  • 枚举登记

我们通过代码先实现枚举登记。

public enum SingletonInEnum {
​
    INSTANCE;
​
    private Object data;
​
    public static SingletonInEnum getInstance() {
        return INSTANCE;
    }
​
    public Object getData() {
        return data;
    }
​
    public void setData(Object data) {
        this.data = data;
    }
​
}

写法相当的简单,setData完成实例化对象的登记。

我们先用序列化去破坏这个单例。

​
    public static void main(String[] args) {
        SingletonInEnum instance1;
        SingletonInEnum instance2 = SingletonInEnum.getInstance();
        instance2.setData(new Object());
        try(
             FileOutputStream fos = new FileOutputStream("out.se");
             ObjectOutputStream oos = new ObjectOutputStream(fos);
             FileInputStream fis = new FileInputStream("out.se");
             ObjectInputStream ois = new ObjectInputStream(fis);
            ){
            oos.writeObject(instance2);
            oos.flush();
            instance1 = (SingletonInEnum)ois.readObject();
            System.out.println(instance1.getData());
            System.out.println(instance2.getData());
            System.out.println(instance1.getData() == instance2.getData());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
​
    }

运行结果如下。

reg1.png

结果显示,反序列化的实例对象跟原本的对象引用地址一致,是同一个对象。原理是什么呢?通过查看ObjectInputStream类的readEnum方法,我们发现了枚举类型其实是通过类名和Class对象找到一个唯一的枚举对象。因此,枚举对象是不可能被类加载器加载多次的。

序列化是不能破坏单例了,那么反射呢?

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
​
        final Class<SingletonInEnum> singletonInEnumClass = SingletonInEnum.class;
        final Constructor<SingletonInEnum> declaredConstructor = singletonInEnumClass.getDeclaredConstructor();
        declaredConstructor.newInstance();
}

结果如下。

reg2.png

报的异常不是很常见,NoSuchMethodException,这个异常的含义是没有找到无参的构造方法。

通过查看enum源码,确实只有一个访问权限是protected的构造方法。

protected Enum(String name,int ordinal){
    this.name = name;
    this.ordinal = ordinal;
}

JVM语法规则是不允许通过反射来创建枚举类型,在反射中用到的newInstance方法,在这个方法中做了强制性的判断,如果修饰符是Modifier.ENUM枚举类型,直接抛出异常。 所以没可能用反射去创建枚举类型。

枚举单例因为枚举enum语法的特殊性,实现了特别巧妙的单例,它也是比较推崇的一种实现方式。

另外一种注册式单例就是用容器缓存去实现。原理不复杂,建议参考Spring容器。