可能有人看了我上一篇文章里几种方式对比的表格,觉得枚举有缺点,为什么Joshua Bloch还推荐使用枚举?
这就要提到单例的破解了。普通的单例模式是可以通过反射和序列化/反序列化来破解的,而Enum由于自身的特性问题,是无法破解的。当然,由于这种情况基本不会出现,因此我们在使用单例模式的时候也比较少考虑这个问题。
枚举类是实现单例模式最好的方式
在单例模式的实现中,除去枚举方法实现的单例模式,其它的实现都可以利用反射构造新的对象,从而破坏单例模式,但是枚举就不行,下面说说原因:
破坏单例的方式有 3 种,反射、克隆以及序列化,下面详细介绍:
反射
常见的单例模式实现中,往往有一个私有的构造函数,防止外部程序的调用,但是通过反射可以轻而易举的破坏这个限制:
显然,通过反射可以破坏所有含有无参构造器的单例类,如可以破坏懒汉式、饿汉式、静态内部类的单例模式。
但是反射无法破坏通过枚举实现的单例模式,利用反射构造新的对象,由于 enum 没有无参构造器,结果会抛出 NoSuchMethodException 异常。
枚举安全的原因解释:
对 EnumSingleton 文件进行反编译,可以发现 EnumSingleton 继承于 Enum,而 Enum 类确实没有无参的构造器,所以抛出 NoSuchMethodException。
进一步,通过调用父类有参构造器构造枚举实例对象,样例程序又抛出 IllegalArgumentException 异常。
因为 Constructor 的 newInstance 方法限定了 clazz 的类型不能是 enum,否则抛出异常。
所以枚举类不能通过反射构建构造函数的方式构建新的实例。
序列化
先看看通过序列化破坏单例的例子,其中 Singleton 实现了 Serializable 接口,才有可能通过序列化破坏单例。
枚举类实现,枚举类不实现 Serializable 接口,都可以进行序列化,并且返回原来的单例。
原因: 枚举类的 writeObject 方法仅仅是将 Enum.name 写到文件中,反序列化时,根据 readObject 方法的源码定位到 Enum 的 valueOf 方法,他会根据名称返回原来的对象。
克隆
实现 Cloneable 接口重写 clone 方法,但是 Enum 类中 clone 的方法是 final 类型,无法重写,也就不能通过克隆破坏单例。
不用枚举如何防止单例模式破坏
若实现了序列化接口,重写 readResolve 方法即可,反序列化时将调用该方法返回对象实例。
通过反射破坏单例的场景,可以在构造方法中判断实例是否已经创建,若已创建则抛出异常。
通过clone破坏单例的场景,可以重写clone方法,返回已有单例对象。