- new,无风险。由于单例模式提供的构造函数都是私有的,所以不能在外部使用new的方式创建对象。
- 克隆,无风险。因为对象必须实现一个Cloneable 接口才可以直接调用clone(),而单例模式并未实现Cloneable 接口,所以,无风险。
- 序列化机制,有风险。对象序列化成一个字节流后,若要被反序列化恢复时,会生成一个新的对象,此对象和原来的对象具有一模一样的行为,但归根结底是两个对象。
- 反射机制,有风险。有句老话“反射可以打破一切封装!”,说明了任何类在反射机制面前都是透明的,通过反射机制可以获得类的各种属性,当然也可以获得类的构造函数(包括私有的),从而构造一个新的对象。但是,枚举类除外,下文会提到。
通过反射破坏单例
private static void reflectMethod() {
try {
EagerSingleton instance1 = EagerSingleton.getInstance();
// 通过反射得到其构造方法,修改其构造方法的访问权限,并用这个构造方法构造一个对象
Constructor constructor = EagerSingleton.class.getDeclaredConstructor();
// 把私有访问域的访问级别设置为public,否则,会抛异常
constructor.setAccessible(true);
EagerSingleton instance2 = (EagerSingleton) constructor.newInstance(); System.out.println( "反射是否破坏了单例 : " + !(instance1 == instance2)); // true
} catch (Exception e) {
e.printStackTrace();
}
}
显然,说好的单例已经变成了多例。防止反射破坏单例可以使用枚举式单例模式。
private static void reflectEnumMethod() {
try {
EnumSingleton instance1 = EnumSingleton.INSTANCE;
Constructor constructor = EnumSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
EnumSingleton instance2 = (EnumSingleton) constructor.newInstance();
System.out.println( "反射是否破坏了单例 : " + !(instance1 == instance2)); // true
} catch (Exception e) {
e.printStackTrace();
}
}
执行改方法时抛出如下异常:
java.lang.NoSuchMethodException: com.east7.singleton.EnumSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.east7.controller.SingletonController.reflectEnumMethod(SingletonController.java:42)
at com.east7.controller.SingletonController.main(SingletonController.java:22)
不言而喻,我们在代码中无法通过反射获取 enum 类的构造方法。对于枚举,JVM 会自动进行实例的创建,其构造方法由 JVM 在创建实例的时候进行调用。
通过序列化机制破坏
private static void serialMethod() {
try {
EagerSingleton instance1 = EagerSingleton.getInstance(); // instance3 将从 instance1 序列化后,反序列化而来
EagerSingleton instance3 = null;
ByteArrayOutputStream bout = null;
ObjectOutputStream out = null;
try {
bout = new ByteArrayOutputStream();
out = new ObjectOutputStream(bout);
out.writeObject(instance1); ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
ObjectInputStream in = new ObjectInputStream(bin);
instance3 = (EagerSingleton) in.readObject();
} catch (Exception e) {
System.out.println(" -------------- " + e);
} finally {
// close bout&out
}
System.out.println( "序列化是否破坏了单例 : " + (instance1 == instance3));
} catch (Exception e) {
e.printStackTrace();
}
}
执行结果打印为false,说明生成了不同的实例。序列化饿汉式单例:
import java.io.ObjectStreamException;
import java.io.Serializable; /**
* 序列化的饿汉式单例,线程安全
*/
public class EagerSingletonPlus implements Serializable { private static final EagerSingletonPlus instance = new EagerSingletonPlus();
// 私有化构造方法 private EagerSingletonPlus() {
}
public static EagerSingletonPlus getInstance() {
return instance;
}
/**
* 看这里,新增
*/
public Object readResolve() throws ObjectStreamException {
return instance;
} // 序列化,新增
private static final long serialVersionUID = -3006063981632376005L;
}
把serialEnumMethod中的EagerSingleton类替换成EagerSingletonPlus,再次执行,结果变成了false,说明反序列化生成的实例instance3和instance1对应同一个实例。因为在反序列化的时候,JVM 会自动调用 readResolve() 这个方法,我们可以在这个方法中替换掉从流中反序列化回来的对象。关于readResolve()的更详细信息,请参考3.7 The readResolve Method 。下面验证基于枚举创建的单例类是不会被序列化破坏的,代码结构和serialMethod()类似。
private static void serialEnumMethod() {
try {
EnumSingleton instance1 = EnumSingleton.INSTANCE;
// instance3 将从 instance1 序列化后,反序列化而来
EnumSingleton instance3 = null;
ByteArrayOutputStream bout = null;
ObjectOutputStream out = null;
try {
bout = new ByteArrayOutputStream();
out = new ObjectOutputStream(bout);
out.writeObject(instance1); ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
ObjectInputStream in = new ObjectInputStream(bin);
instance3 = (EnumSingleton) in.readObject();
} catch (Exception e) {
System.out.println(" - EagerSingletonPlus ------------- " + e);
} finally {
// close bout&out
}
System.out.println( "枚举自带光环,反序列化未破坏单例 : " + (instance1 == instance3)); // true
} catch (Exception e) {
e.printStackTrace();
}
}
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 const " + enumType +"." + name);
}
从代码中可以看到,代码会尝试从调用enumType这个Class对象的enumConstantDirectory()方法返回的map中获取名字为name的枚举对象,如果不存在就会抛出异常。再进一步跟到enumConstantDirectory()方法,就会发现到最后会以反射的方式调用enumType这个类型的values()静态方法,也就是上面我们看到的编译器为我们创建的那个方法,然后用返回结果填充enumType这个Class对象中的enumConstantDirectory属性。所以,JVM对序列化有保证。