面试官: 能绕过单例模式得到多个不同实例吗?
猴哥有次被面试到这个问题,听到这个问题,有点懵,既然设计成单例模式,就是不想让随便实例化了,还去反单例,是不是闲着DT,面试官非要这么搞,咱也没办法啊,莫非想搞啥坏事?想了想,得到对象的实例,一般就是new ,再者就是反射了,但是单例对象一般都经过私有化了,new 肯定是走不通了,如果通过反射改变私有构造方法的访问权限,应该就可以拿到对象实例了,原来JDK反射提供了此方法,通过设置 setAccessible(true),即可获取单例对象。
通过反射破坏单例
package com.monkeyjava.learn.basic.design;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* @author monkeyjava
* @description
* @date 2021/09/23
*/
public class Singleton {
private final static Singleton SINGLETON = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return SINGLETON;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException,
InstantiationException {
Singleton s1 = Singleton.getInstance();
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton s2 = constructor.newInstance();
System.out.println("单例对象" + s1);
System.out.println("反射获取单例对象"+ s2);
}
}
输出结果:
单例对象com.monkeyjava.learn.basic.design.Singleton@610455d6
反射获取单例对象com.monkeyjava.learn.basic.design.Singleton@511d50c0
输出结果显示这两个不是同一个对象, 虽然不是同一个对象,s2对象是通过反射得到的实例,只不过单例模式失去了应有的意义,被破坏了而已。
实例化计数防止破坏
既然可以反射得到单例,有什么办法可以防止吗?如果对构造方法加一个计数器,超过2个对象实例化,抛一个异常是否能解决呢?
代码如下
package com.monkeyjava.learn.basic.design;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* @author monkeyjava
* @description
* @date 2021/09/23
*/
public class Singleton {
private final static Singleton SINGLETON = new Singleton();
private static int count;
private Singleton(){
synchronized (Singleton.class) {
if (count > 0) {
throw new RuntimeException("error:创建了两个实例!");
}
++count;
}
}
public static Singleton getInstance(){
return SINGLETON;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException,
InstantiationException {
Singleton s1 = Singleton.getInstance();
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton s2 = constructor.newInstance();
System.out.println("单例对象" + s1);
System.out.println("反射获取单例对象"+ s2);
}
}
加了一个count 计数,并且加了同步锁,首次初始化时count为,并且count++ , count=1,下次初始化时count>0 满足条件,直接触发RuntimeException。运行上面的方法,直接抛出异常.
别急,老哥!反射既然可以修改方法属性,也可以修改变量值啊,通过反射初始化后,再将count 对象值设置为0,一样不也可以初始化吗?防不胜防啊!!代码如下。
package com.monkeyjava.learn.basic.design;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
/**
* @author monkeyjava
* @description
* @date 2021/09/23
*/
public class Singleton {
private final static Singleton SINGLETON = new Singleton();
private static int count;
private Singleton(){
synchronized (Singleton.class) {
if (count > 0) {
throw new RuntimeException("error:创建了两个实例!");
}
++count;
}
}
public static Singleton getInstance(){
return SINGLETON;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException,
InstantiationException, NoSuchFieldException {
// 反射获取count变量
Field countField = Singleton.class.getDeclaredField("count");
countField.setAccessible(true);
Singleton s1 = Singleton.getInstance();
// 实例化对象后,再将count 设置为0
countField.set(s1, 0);
// 反射初始化时count 是0,所以可以正常初始化
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton s2 = constructor.newInstance();
System.out.println("单例对象" + s1);
System.out.println("反射获取单例对象"+ s2);
}
}
运行结果
单例对象com.monkeyjava.learn.basic.design.Singleton@511d50c0
反射获取单例对象com.monkeyjava.learn.basic.design.Singleton@60e53b93
此刻是不是感觉反射很讨厌,看来反射是解决不了单例破坏问题啊。
通过序列化破坏
package com.monkeyjava.learn.basic.design;
import java.io.*;
/**
* 通过使用序列化的方式失效
* @author monkeyjava
* @description
* @date 2021/09/23
*/
public class Singleton2 implements Serializable {
private final static Singleton2 SINGLETON = new Singleton2();
private Singleton2(){
}
public static Singleton2 getInstance(){
return SINGLETON;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
Singleton2 s = Singleton2.getInstance();
FileOutputStream fos = new FileOutputStream("Singleton2.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("Singleton2.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
Singleton2 s1 = (Singleton2)ois.readObject();
System.out.println(s==s1);
}
}
输出结果: false , 证明序列化后的对象已不是先前的对象,出现了2个实例,违法了单例原则。有什么方法解决吗?
增加readResolve方法被防止序列化破坏
package com.monkeyjava.learn.basic.design;
import java.io.*;
/**
* 通过使用序列化的方式失效
* @author monkeyjava
* @description
* @date 2021/09/23
*/
public class Singleton2 implements Serializable {
private final static Singleton2 SINGLETON = new Singleton2();
private Singleton2(){
}
public static Singleton2 getInstance(){
return SINGLETON;
}
private Object readResolve(){
return SINGLETON;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
Singleton2 s = Singleton2.getInstance();
FileOutputStream fos = new FileOutputStream("Singleton2.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("Singleton2.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
Singleton2 s1 = (Singleton2)ois.readObject();
System.out.println(s==s1);
}
}
输出结果: true
防止单例破坏的正确姿势
总结,实现单例模式的唯一推荐方法,使用枚举类来实现。使用枚举类实现单例模式,在对枚举类进行序列化时,还不需要添加readRsolve方法就可以避免单例模式被破坏。序列化获取到的都是同一个对象,感兴趣同学可以验证下,并且无法通过反射获取实例。
package com.monkeyjava.learn.basic.design;
public enum EnumSingleton {
INSTANCE;
public void sayHellow() {
System.out.println("枚举反单例测试");
}
public static void main(String[] args) {
EnumSingleton.INSTANCE.sayHellow();
}
}