设计模式【1.2】-- 枚举式单例有那么好用么?

[TOC]

1. 单例是什么?

单例模式:是一种创建型设计模式,目的是保证全局一个类只有一个实例对象,分为懒汉式和饿汉式。所谓懒汉式,类似于懒加载,需要的时候才会触发初始化实例对象。而饿汉式正好相反,项目启动,类加载的时候,就会创建初始化单例对象。

前面说过单例模式以及如何破坏单例模式,我们一般情况尽可能阻止单例模式被破坏,于是各种序列化,反射,以及克隆的手段,我们都需要考虑进来,最终的代码如下:


import java.io.Serializable;

public class Singleton implements Serializable {
    private static int num = 0;

    // valitile禁止指令重排
    private volatile static Singleton singleton;

    // 禁止多次反射调用构造器
    private Singleton() {
        synchronized (Singleton.class) {
            if (num == 0) {
                num++;
            } else {
                throw new RuntimeException("Don't use this method");
            }
        }
    }

    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    // 禁止序列化的时候,重新生成对象
    private Object readResolve() {
        return singleton;
    }
}

前面提过破坏序列化的四种方式:

  • 没有将构造器私有化,可以直接调用。
  • 反射调用构造器
  • 实现了cloneable接口
  • 序列化与反序列化

2. 枚举的单例可以被破坏么?

但是突然想到一个问题,一般都说枚举的方式实现单例比较好,较为推荐。真的是这样么?这样真的是安全的么?

image

那我们就试试,看看各种手段,能不能破坏它的单例。首先我们来写一个单例枚举类:

public enum SingletonEnum {
    INSTANCE;
    public SingletonEnum getInstance(){
        return INSTANCE;
    }
}

在命令行执行以下的命令看上面的枚举类编译之后到底是什么东西?

javac SingletonEnum.java
javap SingletonEnum
image
public final class singleton.SingletonEnum extends java.lang.Enum<singleton.SingletonEnum> {
  public static final singleton.SingletonEnum INSTANCE;
  public static singleton.SingletonEnum[] values();
  public static singleton.SingletonEnum valueOf(java.lang.String);
  public singleton.SingletonEnum getInstance();
  static {};
}

可以看出,实际上,编译后的代码是继承于Enum类的,并且是泛型。用final修饰,其实也是类,那就是不可以被继承原因。而且INSTANCE也是final修饰的,也是不可变的。但是这样看,上面的都是public方法。那构造方法呢?没有被重写成为private么?

要是没有重写的话,那就很容易破坏单例啊!我们使用javap -p SingletonEnum看看结果:

<img src="https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20201213225659.png" style="zoom:50%;" />

可以看出确实构造函数已经被私有化,那么外部就不能直接调用到构造方法了。那其他方法呢?我们试试放射调用构造器:

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class SingletonTests {
    public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        SingletonEnum singleton1 = SingletonEnum.INSTANCE;
        SingletonEnum singleton2 = SingletonEnum.INSTANCE;
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());

        Constructor<SingletonEnum> constructor = null;
        constructor = SingletonEnum.class.getDeclaredConstructor();
        constructor.setAccessible(true);

        SingletonEnum singleton3 = constructor.newInstance();
        System.out.println(singleton1 == singleton3);
    }
}

执行结果如下:

692404036
692404036
Exception in thread "main" java.lang.NoSuchMethodException: singleton.SingletonEnum.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    at singleton.SingletonTests.main(SingletonTests.java:15)

咦,怎么回事?反射失败了???

<img src="https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20201213231748.png" style="zoom:50%;" />

看起来报错是getDeclaredConstructor()失败了,那我们看看到底有哪些构造器:

    public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        Constructor<SingletonEnum>[] constructor = null;
        constructor = (Constructor<SingletonEnum>[]) SingletonEnum.class.getDeclaredConstructors();
        for(Constructor<SingletonEnum> singletonEnumConstructor:constructor){
            System.out.println(singletonEnumConstructor);
        }
    }

执行结果如下,发现只有一个构造器,里面参数是Stringint,所以啊,反射调用无参数构造器肯定也是如此。

private singleton.SingletonEnum(java.lang.String,int)

毕竟它是继承于Enum的,那我猜想它大概也只有这个方法,验证以下,打开源码:

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {
  
    private final String name;

    public final String name() {
        return name;
    }
    private final int ordinal;

    public final int ordinal() {
        return ordinal;
    }

    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }


可以看出,这里面只有两个属性:nameordinal,构造器被重写了,正是Stringint,验证了我们的猜想,也就是没有办法使用无参数构造器来构造出破坏单例的对象。那要是我们使用有参数构造呢?试试!!!

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class SingletonTests {
    public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        SingletonEnum singleton1 = SingletonEnum.INSTANCE;
        SingletonEnum singleton2 = SingletonEnum.INSTANCE;
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());

        Constructor<SingletonEnum> constructor = null;
        constructor = SingletonEnum.class.getDeclaredConstructor(String.class,int.class);//其父类的构造器
        constructor.setAccessible(true);

        SingletonEnum singleton3 = constructor.newInstance("INSTANCE",0);
        System.out.println(singleton1 == singleton3);
    }
}

结果呢?还是一样的报错,这是什么东东?

692404036
692404036
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at singleton.SingletonTests.main(SingletonTests.java:18)

看起来意思是不能反射创建enum对象,啥?这错误一看,就是Constructor.newInstance()417行抛出来的,我们看看:

    @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        // 限制枚举类型
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

原来反射的源代码中,枚举类型的已经被限制了,一旦调用就会抛出异常,那这条路走不通了,也就证明了反射无法破坏枚举的单例。new对象更是行不通了。

clone呢?打开Enum的源码我们里面就断了这个念头,这里面的clone()方法,已经被final修饰了,不能被子类重写,一调用就抛出异常。所以clone这条路也不可能破坏枚举的单例模式。

    protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

那序列化呢?如果我们序列化之后,再反序列化,会出现什么情况?

import java.io.*;
import java.lang.reflect.InvocationTargetException;

public class SingletonTests {
    public static void main(String[] args) throws Exception, InvocationTargetException, InstantiationException, NoSuchMethodException {
        SingletonEnum singleton1 = SingletonEnum.getInstance();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("file"));
        objectOutputStream.writeObject(singleton1);
        File file = new File("file");
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
        SingletonEnum singleton2 = (SingletonEnum) objectInputStream.readObject();
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }
}

上面的代码执行之后,结果如下:

1627674070
1627674070

说明序列化反序列化回来之后,其实是同一个对象!!!所以无法破坏单例模式。为什么呢?我们来分析一下源码!!!

先看看序列化的时候,实际上调用的是ObjectOutputStream.writeObject(Object obj)

image-20201214102820404

writerObject()Object obj方法里面调用了writeObject0(obj,false),writeObject0(obj,false)里面看到枚举类型的序列化写入:

image-20201214104840375

writeEnum(Enum<?>)里面是怎么序列化的呢?

    private void writeEnum(Enum<?> en,
                           ObjectStreamClass desc,
                           boolean unshared)
        throws IOException
    {
        // 标识是枚举类型
        bout.writeByte(TC_ENUM);
        ObjectStreamClass sdesc = desc.getSuperDesc();
        // 类型描述
        writeClassDesc((sdesc.forClass() == Enum.class) ? desc : sdesc, false);
        handles.assign(unshared ? null : en);
        // 将名字写入name()
        writeString(en.name(), false);
    }

看起来序列化的时候,是用名字写入序列化流中,那反序列化的时候呢?是怎么操作的呢?

    public final Object readObject()
        throws IOException, ClassNotFoundException {
        return readObject(Object.class);
    }

里面调用的是另外一个readObject()方法,readObject()方法其实是调用了readObject0(type,false)

image-20201214110032825

看到反序列化的时候,枚举类型的时候,是怎么实现的呢?里面有一个readEnum():

image-20201214110222514

我们来看看readEnum(),里面其实里面是先读取了名字name,再通过名字Enum.valueOf()获取枚举。

image-20201214110933076

所以上面没有使用反射,还是获取了之前的对象,综上所述,枚举的序列化和反序列化并不会影响单例模式。

3. 总结一下

经过上面一顿分析,枚举不可以直接调用构造函数,不可以反射破坏单例模式,因为内部实现阻止了,实现clone接口也不可以,这个方法已经设置为final。序列化和反序列化的时候,内部没有使用反射去实现,而是查找之前的对象,直接返回,所以还是同一个对象。

这样一来,怪不得《effective java》里面推荐这个写法,既简洁,还能够防止各种破坏,还有不用的理由么?

【作者简介】
秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353

推荐阅读更多精彩内容