Java单例模式和反射、序列化和枚举实现单例
单例模式和反射
单例模式最根本的一点就是一个类只能有一个实例。如果通过反射构建该类的实例,则单例模式被破坏。我们看一个例子:
/**
* 静态内部类式单例模式
*/
class Singleton implements Serializable{
private static class SingletonClassInstance {
private static final Singleton instance = new Singleton();
}
//方法没有同步,调用效率高
public static Singleton getInstance() {
return SingletonClassInstance.instance;
}
private Singleton() {}
}
复制代码
这种单例实现方式相信大家都很熟悉。我们来看看通过反射创建类实例是否会破坏单例。预览模式。主要函数代码如下:
Singleton sc1 = Singleton.getInstance();
Singleton sc2 = Singleton.getInstance();
System.out.println(sc1); // sc1,sc2是同一个对象
System.out.println(sc2);
/*通过反射的方式直接调用私有构造器(通过在构造器里抛出异常可以解决此漏洞)*/
Class<Singleton> clazz = (Class<Singleton>) Class.forName("com.learn.example.Singleton");
Constructor<Singleton> c = clazz.getDeclaredConstructor(null);
c.setAccessible(true); // 跳过权限检查
Singleton sc3 = c.newInstance();
Singleton sc4 = c.newInstance();
System.out.println("通过反射的方式获取的对象sc3:" + sc3); // sc3,sc4不是同一个对象
System.out.println("通过反射的方式获取的对象sc4:" + sc4);
复制代码
我们看一下输出:
com.learn.example.Singleton@52e922
com.learn.example.Singleton@52e922
通过反射的方式获取的对象sc3:com.learn.example.Singleton@25154f
通过反射的方式获取的对象sc4:com.learn.example.Singleton@10dea4e
复制代码
我们看到正常调用getInstance,符合我们的预期。如果是通过反射(绕过检查,可以通过反射调用私有检查),那么单例模式基本上已经失败了。我们创建了两个完全不同的对象 sc3 和 sc4。我们如何解决这个问题呢?对于反射来说,需要调用构造函数,这样我们才能在构造函数中进行判断。修复代码如下:
class Singleton implements Serializable{
private static class SingletonClassInstance {
private static final Singleton instance = new Singleton();
}
//方法没有同步,调用效率高
public static Singleton getInstance() {
return SingletonClassInstance.instance;
}
//防止反射获取多个对象的漏洞
private Singleton() {
if (null != SingletonClassInstance.instance)
throw new RuntimeException();
}
}
复制代码
我们看到的唯一改进是在构造函数中添加了判断。如果当前存在实例,则会抛出异常以防止反射创建对象。让我们看一下输出:
com.learn.example.Singleton@52e922
com.learn.example.Singleton@52e922
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
at java.lang.reflect.Constructor.newInstance(Unknown Source)
at com.learn.example.RunMain.main(RunMain.java:45)
Caused by: java.lang.RuntimeException
at com.learn.example.Singleton.<init>(RunMain.java:28)
... 5 more
复制代码
我们看到,当我们使用反射创建对象时,会抛出异常。
单例模式与序列化
除了反射之外,反序列化过程也会破坏单例模式。现阶段,我们看一下反序列化输出的结果:
com.learn.example.Singleton@52e922
com.learn.example.Singleton@52e922
对象定义了readResolve()方法,通过反序列化得到的对象:com.learn.example.Singleton@16ec8df
复制代码
我们看到,反序列化之后,对象和原来的对象sc1不再是同一个对象了。我们需要处理反序列化过程。处理代码如下:
//防止反序列化获取多个对象的漏洞。
//无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。
//实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象
private Object readResolve() throws ObjectStreamException {
return SingletonClassInstance.instance;
}
复制代码
从注释中我们也可以看出,readResolve方法会覆盖原来的反序列化对象。我们删除原始的反序列化对象并用已经创建的单例对象覆盖它。我们看一下当前的输出:
com.learn.example.Singleton@52e922
com.learn.example.Singleton@52e922
对象定义了readResolve()方法,通过反序列化得到的对象:com.learn.example.Singleton@52e922
复制代码
使用枚举来实现单例
Effective Java建议使用枚举来实现单例,因为使用枚举来实现单例可以避免反射和序列化方面的漏洞。让我们使用下面的例子。我们看一下:
class Resource{}
/**
* 使用枚举实现单例
*/
enum SingletonEnum{
INSTANCE;
private Resource instance;
SingletonEnum() {
instance = new Resource();
}
public Resource getInstance() {
return instance;
}
}
复制代码
我们在main方法中调用代码:
Resource resource1 = SingletonEnum.INSTANCE.getInstance();
Resource resource2 = SingletonEnum.INSTANCE.getInstance();
System.out.println(resource1);
System.out.println(resource2);
复制代码
输出如下:
com.learn.example.Resource@52e922
com.learn.example.Resource@52e922
复制代码
我们看到我们通过枚举实现了单例,那么枚举是如何保证单例的(如何满足多个线程和序列化标准)?事实上,枚举是一个继承自 java.lang.Enum 类的常规类。反编译上述class文件后,我们得到如下代码:
public final class SingletonEnum extends Enum<SingletonEnum> {
public static final SingletonEnum INSTANCE;
public static SingletonEnum[] values();
public static SingletonEnum valueOf(String s);
static {};
}
复制代码
从反编译代码中我们可以看到INSTANCE已经被声明为static。在加载类时,我们可以知道,虚拟机保证类的()方法在多线程环境下被正确锁定和同步。因此,枚举实现在实例化时是线程安全的。
枚举实现和序列化
Java规范规定每个枚举类型和定义的枚举变量在JVM中都是唯一的。因此,Java对枚举类型的序列化和反序列化做了专门的规定。
在序列化过程中,Java 只将枚举对象的 name 属性输出到结果中。反序列化时,使用java.lang.Enum的valueOf()方法根据枚举对象的名称查找枚举对象。
也就是说,以下面的列表为例。序列化期间,仅输出名称 INSTANCE。在反序列化过程中,该名称用于查找对应的枚举类型。因此,反序列化的实例也将与之前序列化的对象实例相同。
Effective Java中的单元素枚举类型被作者认为是实现Singleton的最佳方式。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。