为什么枚举类是单例模式?
概述
在单例模式的实现中, 除了基于 枚举 方法实现的单例模式外, 其他方法都可以借助反射机制生成新的对象以破坏原有的单例模式; 然而, 这种手段对于枚举类型并不奏效, 下文将对此进行详细说明
破坏单例的方式有 3 种,反射、克隆以及序列化,下面详细介绍:
1. 反射
在常见的单例模式实现中,在实现过程中通常会包含一个私有的构造函数以防止外部程序直接调用此类实例。然而,在某些情况下利用反射技术可能会轻易地绕过这一机制带来的潜在风险:
public class DobleCheckSingleton {
private DobleCheckSingleton(){}
private static volatile DobleCheckSingleton dobleCheckSingleton;
public static DobleCheckSingleton getSingleton(){
if (dobleCheckSingleton == null){
synchronized (DobleCheckSingleton.class){
if (dobleCheckSingleton == null){
dobleCheckSingleton = new DobleCheckSingleton();
}
}
}
return dobleCheckSingleton;
}
public static void main(String[] args) {
try {
DobleCheckSingleton dobleCheckSingleton = DobleCheckSingleton.getSingleton();
Constructor<DobleCheckSingleton> constructor = DobleCheckSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
DobleCheckSingleton reflectInstance = constructor.newInstance();
System.out.println(dobleCheckSingleton == reflectInstance);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出:false,单例被破坏
基于枚举机制实现的单例模式中,通过使用反射机制来创建新的对象。然而,在这种情况下,由于没有提供无参数构造器的方法而导致的结果将是程序在运行时抛出 NoSuchMethodException 异常。
public enum EnumSingleton {
INSTANCE;
public static void main(String[] args) {
try {
EnumSingleton enumSingleton = EnumSingleton.INSTANCE;
// 获取无参的构造函数
Constructor<EnumSingleton> constructor = null;
constructor = EnumSingleton.class.getDeclaredConstructor();
// 使用构造函数创建对象
constructor.setAccessible(true);
EnumSingleton reflectInstance = constructor.newInstance();
System.out.println(enumSingleton == reflectInstance);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出:java.lang.NoSuchMethodException: singleton.EnumSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at singleton.EnumSingleton.main(EnumSingleton.java:19)
原因解释
通过反汇编EnumSingleton文件后发现该类继承自Enum类,并且其确实没有无参数构造函数而导致无法调用该方法从而引发NoSuchMethodException
枚举类 EnumSingleton 反编译结果
public final class singleton.EnumSingleton extends java.lang.Enum<singleton.EnumSingleton>
Enum 类的构造方法
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
此外,在利用带有参数的父类构造器构建枚举实例对象时(或:同时),示例程序再次触发了 IllegalArgumentException 异常。
public enum EnumSingleton {
INSTANCE;
public EnumSingleton getInstance(){
return INSTANCE;
}
public static void main(String[] args) {
try {
EnumSingleton enumSingleton = EnumSingleton.INSTANCE;
// 获取无参的构造函数
Constructor<EnumSingleton> constructor = null;
constructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
// 使用构造函数创建对象
constructor.setAccessible(true);
EnumSingleton reflectInstance = constructor.newInstance("test",1);
System.out.println(enumSingleton == reflectInstance);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出:
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at singleton.EnumSingleton.main(EnumSingleton.java:31)
由于 Constructor 的 newInstance 方法规定了 clazz 类型不能为枚举类型,并且导致异常抛出。
@CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
...
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
...
}
所以枚举类不能通过反射构建构造函数的方式构建新的实例
2. 序列化
在分析如何利用序列化破坏单例机制时,在考察实例的过程中发现:Singleton实现了Serializable接口,并因此使得通过序列化破坏单例成为可能。
public class DobleCheckSingleton implements Serializable {
private DobleCheckSingleton() {
}
private static volatile DobleCheckSingleton dobleCheckSingleton;
public static DobleCheckSingleton getSingleton() {
if (dobleCheckSingleton == null) {
synchronized (DobleCheckSingleton.class) {
if (dobleCheckSingleton == null) {
dobleCheckSingleton = new DobleCheckSingleton();
}
}
}
return dobleCheckSingleton;
}
public static void main(String[] args) {
try {
DobleCheckSingleton dobleCheckSingleton = DobleCheckSingleton.getSingleton();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerSingleton.obj"));
oos.writeObject(dobleCheckSingleton);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SerSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
DobleCheckSingleton s1 = (DobleCheckSingleton) ois.readObject();
ois.close();
System.out.println(dobleCheckSingleton == s1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出:false,单例被破坏
枚举类遵循Serializable接口和非Serializable接口时都可以进行序列化处理,并返回原有的单一实例对象。
public enum EnumSingleton {
INSTANCE;
public static void main(String[] args) {
try {
EnumSingleton enumSingleton = EnumSingleton.INSTANCE;
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerSingleton.obj"));
oos.writeObject(enumSingleton);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SerSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
EnumSingleton s1 = (EnumSingleton) ois.readObject();
ois.close();
System.out.println(enumSingleton == s1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出:true
由于:枚举类的 writeObject 方法主要用于将 Enum.name 写入文件。在反序列化过程中,通过分析 readObject 的源代码确定了该枚举类对应的 valueOf 方法。该方法通过名称恢复原始对象。
具体源码参考其它博客分析
3. 克隆
通过重新编写Cloneable接口中的clone方法来实现功能。然而,在Enum类中,默认的clone方法被定义为final类型,因此就无法利用克隆机制破坏单例。
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
}
防止单例模式破坏
一旦实现了序列化接口,则需实现该方法即可;反序列化时将会调用该方法以返回对象实例。
public Object readResolve() throws ObjectStreamException {
return dobleCheckSingleton;
}
利用反射机制破坏单例实例,在构造函数中检测到实例是否已被初始化。如果实例已存在,则会触发异常处理。
private DobleCheckSingleton(){
if (instance !=null){
throw new RuntimeException("实例已经存在,请通过 getInstance()方法获取");
}
}
重写 clone 方法,返回已有单例对象
参考
列举各种方法来实现防止反射的技术手段,并详细分析复制机制以及序列化过程如何破坏单例模式的核心原理
