本文正在参加「Java主题月 - Java Debug笔记活动」,详情查看<活动链接>
单例的几种写法
我在之前的文章中列举了8种单例模式的写法,详见单例模式的8种写法
在开发中,大家可能用的最多的是懒汉式、双重检查这种写法
public class Singleton {
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这种写法,两次判断保证线程安全,懒汉式延迟加载,效率较高,并且在 new Singleton() 新建对象时使用 volatile 可以防止重排序。
看似完美的写法,真的能保证多次获取到的Singleton对象是同一个吗?我们来试验下
正常调用单例获取对象
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1);
System.out.println(singleton2);
}
正常的调用中,输出 singleton1和singleton2 确实是同一个对象
com.test.Singleton@14514713
com.test.Singleton@14514713
Process finished with exit code 0
在这个单例模式中,因为设置了 private 无参构造函数,所以我们无法通过 new Singleton() 直接创建对象来破坏单例的结构
那有没有一种方法可以直接忽略 private ,直接调用 new Singleton() 呢,当然是有的,那就是反射
简单说一下Java反射中常用的api
-
1.获取Class对象
-
1.1. 通过包名+类名
Class<?> aClass = Class.forName("com.test.Singleton");
-
1.2. 通过类名.class
Class<?> aClass = Singleton.class;
-
1.3. 使用getClass()
Class<?> aClass = new Singleton().getClass();
-
-
2.创建类对象
-
2.1. 只能使用默认的无参数构造方法
Singleton singleton = (Singleton) clazz.newInstance();
-
2.2. 可以选择特定构造方法,getConstructor()需要传参
Constructor<?> con = clazz.getConstructor();
Singleton singleton = (Singleton)con.newInstance();
-
-
3.获取类的属性
-
3.1. 获取public的
Field[] fields = clazz.getFields();
-
3.2. 获取所有的,包括private
Field[] declaredFields = clazz.getDeclaredFields();
-
-
4.获取类的方法
-
4.1. 获取类中所有方法,不包括private
Method[] methods = clazz.getMethods();
-
4.1. 获取类中private方法
Method[] declaredMethods = clazz.getDeclaredMethods();
-
-
5.获取类的构造器
-
5.1. 获取无参构造器,包括private
Constructor<?> declaredConstructor = clazz.getDeclaredConstructor();
-
5.2. 获取public构造器
Constructor<?>[] constructors = clazz.getConstructors();
-
5.3. 获取所有构造器,包括private
Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
-
使用反射调用单例获取对象
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1);
System.out.println(singleton2);
Class<?> clazz = Singleton.class;
// 使用反射机制获取构造函数
Constructor<?> constructor = clazz.getDeclaredConstructor();
// 将Accessible设置为true,就可以忽略private访问私有方法或变量的值了,不设置的话会抛出异常
constructor.setAccessible(true);
System.out.println(constructor.newInstance());
}
需要注意的是,通过反射调用 private 方法,需要将 Accessible 设置为 true,否则会执行异常
执行结果
com.test.Singleton@14514713
com.test.Singleton@14514713
com.test.Singleton@69663380
Process finished with exit code 0
从输出结果我们可以看到,这已经不是同一个对象了。因为我们用反射机制破坏了单例的结构,让我们可以直接去 new Singleton()
那么有哪种单例写法可以无视反射呢?那就是枚举
枚举单例模式
枚举为什么是最好的方法,大家可以看下之前的文章单例模式的8种写法中最后一种写法
更为详细的枚举单例模式写法
public class EnumSingleton {
private EnumSingleton() {
}
/**
* 供外界调用
*
* @return EnumSingleton
*/
public static EnumSingleton getInstance() {
return ContainerHolder.HOLDER.instance;
}
private enum ContainerHolder {
HOLDER;
private EnumSingleton instance;
// 饿汉模式
ContainerHolder() {
instance = new EnumSingleton();
}
}
}
测试反射是否能打破这种单例
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, ClassNotFoundException {
// 正常调用
EnumSingleton singleton1 = EnumSingleton.getInstance();
EnumSingleton singleton2 = EnumSingleton.getInstance();
System.out.println(singleton1);
System.out.println(singleton2);
// 反射调用 EnumSingleton
Class<?> clazz = EnumSingleton.class;
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
EnumSingleton enumSingleton = (EnumSingleton) constructor.newInstance();
System.out.println(enumSingleton.getInstance());
// 反射调用内部枚举 ContainerHolder
Class<?> containerHolder = Class.forName("com.wupao.channel.EnumSingleton$ContainerHolder");
// 此处会报错,java.lang.NoSuchMethodException
Constructor<?> declaredConstructor = containerHolder.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
// 反射调用
System.out.println(declaredConstructor.newInstance());
}
输出结果
com.test.EnumSingleton@14514713
com.test.EnumSingleton@14514713
com.test.EnumSingleton@14514713
Exception in thread "main" java.lang.NoSuchMethodException: com.test.EnumSingleton$ContainerHolder.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.test.Test.main(Test.java:60)
Process finished with exit code 1
第三次输出,与前两次输出为同一个对象
而直接通过获取内部枚举类ContainerHolder的方式来创建对象,则会直接报错 NoSuchMethodException,没有这样的方法??
通过查询Enum源码(java.lang.Enum),则会看到Enum有两个带参的构造函数
改下代码,传入两个参数
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, ClassNotFoundException {
// 反射调用内部枚举 ContainerHolder
Class<?> containerHolder = Class.forName("com.wupao.channel.EnumSingleton$ContainerHolder");
// 传入两个参数
Constructor<?> declaredConstructor = containerHolder.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
// 反射调用
System.out.println(declaredConstructor.newInstance());
}
执行结果
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.test.Test.main(Test.java:63)
Process finished with exit code 1
报错写的也很明白,Cannot reflectively create enum objects,不能通过反射来创建枚举对象!
所以,枚举这种方式,是可以防住反射进行破坏的!