创建单例的方式有很多,比如饿汉式、懒汉式、DCL(双重检索机制)以及我们今天讲的枚举方式。
其中只有枚举方式的单例不会被破坏,其他方式都能够使用反射或者序列化的方式破坏单例。
接下来先看看如何破坏,再讲今天的重头戏枚举方式创建单例对象。
如何破坏系统的单例
拿最简单的饿汉式单例模式举例,基于饿汉式的单例如下:
/**
* @author xxy
* @date 2020/7/18
*/
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getSingleton(){
return singleton;
}
}
通过暴力反射破话他,如下所示:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//通过静态方法获取单例
Singleton singleton = Singleton.getSingleton();
//获取Singleton的无参构造器
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
//因为构造器是私有的,需要设置权限
constructor.setAccessible(true);
//使用构造器创建对象
Singleton singleton1 = constructor.newInstance();
//比较是否相等,答案是false
System.out.println(singleton == singleton1);
}
这样就是用反射破坏系统的单例性了。
使用序列化的方式是类似的,同样可以破坏,此处不再举例。
如何避免被破坏呢
避免反射破坏
可以对单例模式进行改造,在构造器中判断是否为空,不是null的话直接抛出异常,修改之后的单例代码如下:
/**
* @author xxy
* @date 2020/7/18
*/
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton() {
if(singleton != null){
throw new RuntimeException("请使用getSingleton()方法获取单例对象");
}
}
public static Singleton getSingleton() {
return singleton;
}
}
此时如果在使用上边所说的反射区创建,会抛出异常:
避免序列化破坏
可以不让单例对象继承Serializable接口,这样就不能序列化了。
使用枚举方式实现单例
使用枚举方式实现单例非常简单,代码如下:
/**
* @author xxy
* @date 2020/7/18
*/
public enum Sing {
INSTANCE;
Sing(){}
public Sing getInstance(){
return INSTANCE;
}
}
测试如下:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Sing instance = Sing.INSTANCE;
Sing instance1 = Sing.INSTANCE;
System.out.println(instance == instance1);
}
结果为:
访问枚举实例时会执行构造方法,同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。
所以使用枚举能够实现懒加载、线程安全。
尝试破坏?
使用反射
那么能够使用上面所说的反射或者序列化破坏他吗?
首先剧透下,如果使用反射的话会报错。
使用反射的方式创建对象如下:
public class test {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException,
InvocationTargetException, InstantiationException {
Sing instance = Sing.INSTANCE;
Class<Sing> singClass = Sing.class;
Constructor<Sing> constructor = singClass.getDeclaredConstructor();
constructor.setAccessible(true);
Sing sing = constructor.newInstance();
System.out.println(instance == sing);
}
}
执行上面代码会报错:
提示:没有上边的构造器。
通过getDeclaredConstructors()方法,可以发现只有一个有参数的构造器,是从java.lang.Enum类得到的,如下:
所以会报错,那我如果获取这个有参数的构造器呢?
public class test {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException,
InvocationTargetException, InstantiationException {
Sing instance = Sing.INSTANCE;
Class<Sing> singClass = Sing.class;
Constructor<Sing> constructor = singClass.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
Sing sing = constructor.newInstance();
System.out.println(instance == sing);
}
}
结果如下:
这是因为反射机制本身导致的,可以来到newInstance()方法中,可以看到:
如果是enum修饰的就会排除非法参数异常。
所以枚举类是不能通过反射来创建的。
使用序列化
这里不举例说明,可以去试一下,即使通过ObjectOutputStream和ObjectInputStream来实现序列化,那么他们序列化之前和之后都是同一个对象,并没有破坏单例。