Java 枚举为什么可以实现单例

文摘   2024-11-15 11:32   江苏  
  1. 前置

  2. 当我们在完成某些业务时,对于固定的东西,多数会定义出一个常量,比如服务的返回码、订单的状态、支付方式等其他维度的抽象。根据实际场景,常量的定义可以用final关键字,也可以用enum,这些都是大家熟知的。我还知道大家都听过:

    Java中,一切皆是对象,所有类都继承自Object。


  3. 入门

    随手创建2个类,Hello和SEASON,代码如下:

    public class Hello {    public static void main(String[] args) {        System.out.println("Hello, World!");    }}public enum SEASON {    SPRING("spring","春天"),    SUMMER("summer","夏天"),    AUTUMN("autumn","秋天"),    WINTER("winter","冬天");    SEASON(String code, String desc) {        this.code = code;        this.desc = desc;    }    //ignore getter setter    private String code;    private String desc;}
    编译出.class文件: javac ${xxx.java}


    查看.class文件的继承关系: javap -v ${xxx.class}



    到这里可以发现Hello的字节码中没有继承类,SEASON 的字节码中显示该类继承自java.lang.Enum。惊不惊喜,意不意外


    我知道你在想啥——如果没有显性的显示继承类,会不会默认继承自Object,有这种直觉的,起码是3年以上的牛马,可以继续证明一下:



    通过简单对Class类的应用,可以看到,在获取Hello的super class时循环了一次,获取SEASON的super class时循环了2次,最后才指向了Object,这才是“所有类都继承自Object”这句话深层次的意思。(如果老师曾告诉你:“当没有显示指定继承类时,默认继承Object”。这话在字节码角度没问题,源码角度个人觉得表达就有些欠妥了,此刻你想说______)。


  4. 串联

    在做一个业务时定义了一个枚举,想着也简单,就没有用IDE快捷键生成构造函数,纯手打的过程中无意触发了一个提示:


    为什么枚举的构造方法不能是public的?我们知道private修饰的话,外部就不能实例化这个类了,可这就要先从单例模式讲起了......


    单例模式:

    单例的意思就是在整个JVM中只有一个相关类型的类对象。在日志处理、配置管理、数据库连接池等场景,控制了实例的数量,减少了系统开销、可以防止对资源的多重占用。

    在Java中,类是一种抽象的数据类型,用于描述具有相同属性和行为的一组对象的集合。单例不允许外部实例化去修改它的属性或行为,因为内部定义的属性或行为做着特定的业务逻辑,如果变化了,程序能不能跑另说,相当于修改内部的定制的一些逻辑(如同西游记中六耳猕猴变成孙悟空想要代替真正的孙悟空窃取金身正果),这还得了


    实现方式:

    很多同学有意或无意都看过设计模式相关的资料,它的实现方式大概有以下几种:

    public class Single {    private Single(){    }    //a.饿汉式:我很饿,先给我搞点吃的     // 缺点:可能根本用不到,一直在JVM中浪费资源    private static Single instance = new Single();    public static Single getInstance(){        return instance;    }    //b.懒汉式:老师布置的作业不到上学前我是不会写的     // 缺点:多线程环境下会有多个实例产生,违反单例初衷    private static Single instance = null;    private static Single getInstance(){        if (null == instance){            instance = new Single();        }        return instance;    }    //c.synchronized 方式:多线程是吧 synchronized 阁下该如何应对,很难么?    // 缺点:锁的粒度太大,影响性能    private static Single instance = null;    public static synchronized Single getInstance(){        if(null == instance){            instance = new Single();        }        return instance;    }    //d.double check : 锁在方法上 的确 影响有点大,我锁代码块,总该收声了吧    // 缺点:忽略了CPU调度是按时间片来的,视觉上的连续性不代表计算机CPU的连续性,有可能在第一个if判断后,CPU调度到其他线程,导致其他线程也进入if判断,产生多个实例    private static Single instance = null;    public static Single getInstance(){        if(null == instance){            synchronized (Single.class){                instance = new Single();            }        }        return instance;    }    //e.double check + volatile : 你们都是小学生,我是大学生,我要用volatile来保证可见性    // 缺点:volatile 会屏蔽掉JVM的一些优化,性能会有所下降    public static volatile Single instance = null;    public static Single getInstance(){        if(null == instance){            synchronized (Single.class){                if(null == instance){                    instance = new Single();                }            }        }        return instance;    }    //f.holder内部类方式:我是一个内部类,我只有在被调用的时候才会被加载,我是懒汉式的优化版    //缺点:不够直观    private static class Holder{        private static final Single instance = new Single();    }    public static Single getInstance(){        return Holder.instance;    }    //g.枚举方式:我是枚举,我是线程安全的,我是最好的单例模式    //缺点:高端的功能,往往只需要最简单的方式    public enum Single1{        INSTANCE;        public static Single1 getInstance(){            return INSTANCE;        }    }}



    从懒汉模式起,后面的很多方式都是在不断解决线程安全问题,碰巧也引入了别的问题以前总是不明白为什么资料上都用枚举来实现单例,这二者有什么关系,直到上面那个错误提示:枚举构造方法不能用public修饰,多少跟这个有点关系吧。


  5. 深入

    上面说单例是保证JVM中只有一个类的实例对象,多例自然就是N个,可以随意由外部改变属性或方法后写入JVM。但如果想要类似于单例,有的类可能需要固定个数的特定的实例化对象,如性别(男 || 女)、季节(春||夏||秋||冬)、星期(一~日),这个时候就应当限制对象的实例化,而不是由用户随意的实例化对象。这时就可以使用多例设计模式,在类的内部提供好实例化对象后进行类的封装,然后提供给用户固定的实例化对象:

    //定义一个类public class Season1 {    //ignore getter and setter    private String season;    private String desc;    public static final Season1 SPRING = new Season1("spring","春天");    public static final Season1 SUMMER = new Season1("summer","夏天");    public static final Season1 AUTUMN = new Season1("autumn","秋天");    public static final Season1 WINTER = new Season1("winter","冬天");    //构造方法私有化,在类的外部无法进行对象的实例化    private Season1(String season,String desc) {        this.season = season;        this.desc = desc;    }}
    public class Hello { public static void main(String[] args) throws Exception { Season1 spring = Season1.SPRING; //客户端直接调用 System.out.println(spring.getDesc()); //春天 }}

    大家可能发现了一个问题,上面这种方法,和单例相比,本质就是在哪儿new 对象的问题。为了解决单例设计模式和多例设计模式的缺陷问题,自JDK1.5之后,Java提供了通过enum关键字定义的枚举结构,对多例设计模式进行简化。使用enum关键字定义枚举类时的注意事项:

    1. 枚举对象以及属性必须放在枚举类内容的首部定义

    2. 枚举对象的命建议采用大写,多个单词采用'_'连接,如INT_MAX

    3. 枚举对象的创建需要对应相应的构造器

    4. 当枚举类型为无参时,可以省去实参列表和括号的书写

    5. 每一个枚举类型都默认为public static final类型,所有可以直接通过类名.属性访问枚举对象

    6. 使用enum关键字定义的枚举类默认继承自Enum父类,且被final关键字修饰

    7. 由于继承自父类且被关键字final修饰,由Java中的单继承机制和final关键字的作用可以知道enum定义的枚举类不能再继承其他类同时不能被子类继承。但是eunm关键字定义的枚举类可以实现接口


  6. 反射

    作为Java高级功能,这部分在学的时候就比较难懂!感兴趣的转场到《Java 反射如何实现接口参数校验》、《反射-提高团队效率》这两篇,快速浏览一下看看能不能模仿 。

    反射是指在运行时动态加载类并获取类的信息(包括方法、属性、构造函数等),并能操作类的属性和方法的一种能力。

    前面单例的实现方式中,都可以通过反射中的setAccessible()方法将private修饰的构造函数访问权限放开。听起来属实很牛,但在枚举面前,这根魔法棒就失效了!因为跟上面定义一样,既然是1.5引入的特殊类型,自然也有别的规则,不然逻辑无法自洽,又怎么形成闭环


    枚举能实现单例,那么破坏单例模式的三种方式:反射、反序列化、克隆,这些都有可能导致JVM中有多个实例,这枚举能受得了么?


    First blood——反射:



    上面简单的演示了反射创建对象,Hello是可以的,但SEASON不行,为什么呢?前面说了王八屁股——“龟定”。那这个错出现在什么地方呢?也许、可能、大概在某段源码里面,但巧合的是,我不小心找到了:(java.lang.reflect.Constructor#newInstance),赤裸裸的写着不准使用反射创建Enum对象


    Double kill——反序列化:

    实现序列化的方式有很多种,这里如果你有学(hua)习(shui)的时间,可以穿越到《序列化与反序列化》逛逛

    这个比较好理解,序列化后的数据可以再通过反序列化变回原来的样子,如果有的用户想要绕过JVM,在运行时动态的反序列化枚举类型的数据,这个能行么?



    源码方面已经考虑到了,左侧ArrayList类型允许,右侧Enum就暴力地阻止了这种事情的发生。你们用我JDK,还想绕过我搞事情,找茬儿是不


    Triple kill——克隆:

    clone 方法是Object类提供的,所有对象都可以调用clone方法,前提是需要实现Cloneable接口标识一下,否则会报错:



    上面以Hello为例通过clone方式得到了2个不一样的对象,但想以类似的方式操作SEASON时直接在编译时就会报错。可以看到enum是不被允许重写clone(),因为Enum类已经将clone()方法定义为final了,并且Enum在使用clone()时直接抛出异常,这就是枚举为什么能防止克隆破环的原因,它根本就不允许克隆。


  7. 拓展

    枚举默认也是static final 修饰,那么要定义常量到底该如何选呢?这取决于当下的场景:比如有一个功能采集用户信息,DB定义了性别字段,备注0-男、1-女:

    public enum Gender {    MALE(0,"男人要有阳刚之气"),    FEMALE(1,"女人要有阴柔之美");        private Integer code;    private String desc;
    Gender(Integer code, String desc) { this.code = code; this.desc = desc; }}

    入参定义了用枚举接收性别,则一定要传入上面定义的MALEFEMALE才能通过,如果用数字,则需要在代码中增加判断数字是否合理,还有可能会绕过某些逻辑或触发某些BUG,这点我希望你永远也遇不到


  8. 总结

    现在你知道为什么单例模式的实现方式中,推荐用枚举实现了吧。在后面的中,可以看到我用枚举配合完成了策略模式的实现。

    突然想到:大道至简和less is more在此刻有点意境相合了


吾生也有涯,而知也无涯。

——《庄子.养生主》

晚霞程序员
一位需要不断学习的30+程序员……