解析——为什么单元素的枚举类型是单例模式的最佳实现

若转载请注明出处!!!解析——为什么单元素的枚举类型是单例模式的最佳实现

目录

1、序列化及反序列化对单例的破坏
2、反射机制对单例的破坏
3、使用枚举类型实现单例模式的原因
4、使用readResolve方法解决反序列化及其弊端

关于单例模式,我们可以使用静态内部类、双重检测的实现方法来保证线程安全,那么该如何保证单例模式最核心的作用——“实现该模式的类有且只有一个实例对象”呢?
我们知道,创建一个对象的方式有:new、克隆、序列化、反射。

  • 由于单例模式提供的是一个私有的构造函数,所以不能外部使用new的方式创建对象。
  • 虽然clone()是Object的方法,也就是说每个对象都拥有一个克隆方法,但是某一个对象直接调用clone方法,会抛出异常,即并不能成功克隆一个对象。调用该方法时,必须实现一个Cloneable 接口。这也就是原型模式的实现方式。还有即如果该类实现了cloneable接口,尽管构造函数是私有的,他也可以创建一个对象。即clone方法是不会调用构造函数的,他是直接从内存中copy内存区域的。所以克隆方式也不需要担心。
    参考: https://blog.csdn.net/chao_19/article/details/51112962?utm_source=copy
  • 一个对象序列化成一个字节流后,若要被反序列化恢复时,会生成一个新的对象,此对象和原来的对象具有一模一样的状态,但归根结底是两个对象。
  • java中提供了反射机制,有句老话“反射可以打破一切封装!”,说明了任何类在反射机制面前都是透明的,通过反射机制可以获得类的各种属性,当然也可以获得类的构造器(就算是私有也没用),从而构造一个新的对象。(但是枚举类除外,下文会提到)。

通过上述分析,若要实现一个完美的单例模式必须考虑序列化和反射问题。本文就序列化和反射是如何破坏单例模式,以及枚举类型是如何完美解决这个问题加以解析讨论。

1、序列化及反序列化对单例的破坏

1.1、场景实例

先写一个双重检测实现的单例模式。
在这里插入图片描述
再写一个测试:
在这里插入图片描述系统输出:
在这里插入图片描述

1.2、场景分析

为什么反序列化之后会生成一个新的对象,要分析这个问题,就要从源码中找答案。这里我们简要分析一下ObjectInputStream.java的源码。
首先给出一个方法调用栈:readObject—>readObject0—>readOrdinaryObject
在这里插入图片描述
分析源码中最重要的语句:

obj = desc.isInstantiable() ? desc.newInstance() : null;

其中desc是一个ObjectStreamClass类对象,再来看看isInstantiable()和newInstance()的源码实现。

boolean isInstantiable() {
        return (cons != null);
    }
Object newInstance()
        throws InstantiationException, InvocationTargetException,
               UnsupportedOperationException
    {
        if (cons != null) {
            try {
                return cons.newInstance();
            } catch (IllegalAccessException ex) {
                // should not occur, as access checks have been suppressed
                throw new InternalError(ex);
            }
        } else {
            throw new UnsupportedOperationException();
        }
    }

ObjectStreamClass类中维护了一个Constructor私有变量cons:

private Constructor<?> cons;

如果某个类(本例中是Singleton04)是可序列化的,也就是实现了Serializable接口,那么cons将会在ObjectStreamClass的构造好书中被初始化成该类的一个无参构造器:

cons = getSerializableConstructor(cl);//c1就是Singleton04

由此往上推,desc.isInstantiable()将返回true,desc.newInstance()将返回Singleton04类的一个实例对象。至此说明了单例模式在反序列化的过程中将产生一个新的对象,单例模式被破坏。

2、反射机制对单例的破坏

在这里插入图片描述
系统输出结果;
在这里插入图片描述
由此可见,即便单例模式将构造函数声明为私有的,通过反射机制依然可以调用该私有构造器来创建对象。即便是在构造其中设置“防止多次实例化的代码”(比如设置一个flag)也于事无补,因为任然可以通过反射机制来修改flag,从而达到多次实例化的目的。
在这里插入图片描述

3、使用枚举类型实现单例模式的原因

3.1、场景实例

首先来写一个枚举类的单例模式实现:
在这里插入图片描述
写一个测试来分析反序列化结果:
在这里插入图片描述
结果:
在这里插入图片描述

再写一个测试来分析反射创建对象的结果:
在这里插入图片描述
结果为:
在这里插入图片描述
说明反射机制创建不了该类对象。在分析原因之前我们先看看枚举类实际上是怎么回事,通过反编译软件(我用的是DJ JAVA COMPLIER)对上述实现的单例模式的字节码文件进行反编译,其结果如下:
在这里插入图片描述
其中的values()方法保存了所有创建的枚举对象,在反序列化时发挥了重要的作用,下文会提到。
再来看看父类Enum的构造函数:
在这里插入图片描述
总结起来,所谓枚举其实是继承了Enum类的一个子类,枚举中可以声明多个对象,每个枚举对象拥有两个唯一的属性:String name 和 int ordinal,name就是我们在声明枚举变量是的名字(比如INSTANCE),ordinal就是声明的顺序(比如INSTANCE是第一个声明的,所以为0)。
明白这些就可以继续分析。

3.2、枚举类型防止反序列化创建新对象原理

在java规范中对枚举类型的序列化和饭序列化做了特殊规定:

Enum constants are serialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not present in the form. To serialize an enum constant, ObjectOutputStream writes the value returned by the enum constant’s name method. To deserialize an enum constant, ObjectInputStream reads the constant name from the stream;the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method,passing the constant’s enum type along with the received constant name as arguments. Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream.The process by which enum constants are serialized cannot be customized: any class-specific writeObject,readObject, readObjectNoData, writeReplace, and readResolve methods defined by enum types are ignored during serialization and deserialization. Similarly, any serialPersistentFields or serialVersionUID field declarations are also ignored–all enum types have a fixedserialVersionUID of 0L. Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent.

首先,在序列化和反序列化期间,任何特定于类的writeObject,readObject,readObjectNoData,writeReplace和readResolve方法都会被忽略。 同样,任何serialPersistentFields或serialVersionUID字段声明也会被忽略,所有枚举类型的fixedserialVersionUID都是0L。(也就是说枚举类型序列化反序列化机制与其他类型的不一样)。
其次,枚举对象的序列化、反序列化有自己的一套机制。序列化时,仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf()方法来根据名字查找枚举对象。
下面分析一下valueOf源码。
在这里插入图片描述
再来看看enumConstantDirectory()源码:
在这里插入图片描述
继续看getEnumConstantsShared()源码:
在这里插入图片描述
getEnumConstantsShared()方法获取枚举类的values()方法,然后得到枚举类所创建的所有枚举对象。
之前提到过,每个枚举对象都有一个唯一的name属性。序列化只是将name属性序列化,在反序列化的时候,通过创建一个Map(key,value),搭建起name和与之对应的对象之间的联系,然后通过索引key来获得枚举对象。
下面梳理一下枚举对象序列化,以及反序列化拿到对象的流程:
序列化:

  • 将枚举对象的name属性进行序列化保存,枚举对象不进行序列化。

反序列化:

  • 通过反射机制获得枚举的class对象
  • 调用getEnumConstantsShared(),其中再调用getMethod()方法等到values()方法,返回一个保存了所有已创建的枚举对象的数组。
  • 调用enumConstantDirectory()方法,用已得到的枚举对象数组构造一个Map(key,value),其中key为name,value为与之对应的枚举对象。
  • 调用valueOf()方法,用反序列化的name属性在Map中索引到对应的枚举对象。
  • 反序列化完成。
    从以上分析可知,枚举在反序列化的过程中并没有创建新的对象,而通过name属性拿到原有的对象,,因此保证了枚举类型实现单例模式的序列化安全。

3.3、枚举类型防止反射机制创建新对象原理

通过反射机制不会创建新的枚举对象的原因就比较简单了,直接从Constructor类源码中便可看出:
在这里插入图片描述
以上说明了底层源码是不允许通过反射机制创建一个枚举对象的,因此保证了枚举类型实现单例模式的反射安全。

4、使用readResolve方法解决反序列化及其弊端

其实如果不使用枚举类实现单例,还有一种方法可以“保证”反序列化安全,那就是在类中类定一个readResolve方法,那么在反序列化之后,新建对象上的readResolve方法就会被调用。然后,该方法返回的对象引用将被返回,取代新建的对象(新建的对象也就是反序列化新生成的对象)。具体怎么回事呢?看源码:
在这里插入图片描述
下面我们具体看看这个readResolve()方法怎么使用。在原来的双检测单例模式中添加一个readResolve()方法:
在这里插入图片描述
再测试一下反序列化结果:
在这里插入图片描述
在这里插入图片描述
由此可见,若类中定义一个readResolve方法,其返回值将会替代之前创建的新对象,一次保证了反序列化后仍然是原来的对象。
但是与枚举实现相比,还是枚举实现具有更高的优先级。具体原因可以参考这篇文章:Effective Java之对于实例控制,枚举类型优于readResolve(七十七)

以上就是本文的全部分析,若有不足或不对之处望大家指出!

  • 15
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值