theme: channing-cyan
这是我参与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(); } }
运行结果如下。
结果显示,反序列化的实例对象跟原本的对象引用地址一致,是同一个对象。原理是什么呢?通过查看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(); }
结果如下。
报的异常不是很常见,NoSuchMethodException
,这个异常的含义是没有找到无参的构造方法。
通过查看enum
源码,确实只有一个访问权限是protected
的构造方法。
protected Enum(String name,int ordinal){ this.name = name; this.ordinal = ordinal; }
JVM
语法规则是不允许通过反射来创建枚举类型,在反射中用到的newInstance
方法,在这个方法中做了强制性的判断,如果修饰符是Modifier.ENUM
枚举类型,直接抛出异常。 所以没可能用反射去创建枚举类型。
枚举单例因为枚举enum
语法的特殊性,实现了特别巧妙的单例,它也是比较推崇的一种实现方式。
另外一种注册式单例就是用容器缓存去实现。原理不复杂,建议参考Spring
容器。