枚举居然还可以实现单例??

本文探讨了枚举在编程中的优势,包括增强代码可读性、线程安全的单例实现、在switch语句中的优势。枚举在超过两个常量定义和特定场景下比传统常量更适用。枚举可直接与数据库交互,避免了equals的双重判断,且在编译和修改时具有优势。枚举在工作中的应用包括更改枚举属性、获取所有枚举类型以及根据属性值获取枚举。枚举实现单例模式既简单又解决了线程安全问题,优于传统的双重校验锁实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

平时在工作的开发中, 发现同事用枚举用的特别溜, 你说自己知道吗? 感觉枚举自己是会用的, 但是自己觉得就是不够深刻, 应该是内心没有真正的掌握掉这个东西, 所以总是无意识的逃避使用这个, 或者说有时候真的想不起来用这个东西.

1. 枚举的优势

那接下来不得不来叨叨枚举的优势了, 它到底有什么好的, 让那么多人用这个呢?

  1. 增强代码可读性(装逼神器)

不得不说, 如果你用了枚举, 立马感觉你跟一般的程序员不一样了, 这个绝对是你装逼和吹牛逼的一个神器吧(嘿嘿)

  1. 去除equals两者判断 由于常量值地址唯一,使用枚举可以直接通过“==”进行两个值之间的对比,性能会有所提高。

  2. 编译优势(与常量类相比)

常量类编译时,常量被直接编译进二进制代码中,常量值在升级中变化后,需要重新编译引用常量的类,因为二进制代码中存放的是旧值。枚举类编译时,没有把常量值编译到代码中,即使常量值发生改变,也不会影响引用常量的类。

  1. 修改优势(与常量类相比)

枚举类编译后默认final class,不允许继承可防止被子类修改。常量类可被继承修改、增加字段等,易导致父类不兼容。

  1. 枚举型可直接与数据库交互。

  1. Switch语句优势

使用int、String类型switch时,当出现参数不确定的情况,偶尔会出现越界的现象,这样我们就需要做容错操作(if条件筛选等),使用枚举,编译期间限定类型,不允许发生越界。

2. 常用场景

抛开业务谈设计, 一切都是扯淡, 所以你必须知道枚举的应用场景, 最重要的是你应该知道什么时候用它, 否则, 就算你知道怎么用, 但是不知道该用它了, 它还是摆设.

2.1 如果超过两个以上的常量的定义, 可以考虑使用枚举

一般我们定义常量都是需要public static final.... , 现在你要是会使用枚举, 直接换一个高逼格的枚举来搞定吧, 而且枚举也提供了比常量更多的方法.

public enum Color {  
  RED, GREEN, BLANK, YELLOW  
}

2.2 在switch语句中, 可以考虑使用枚举

enum Signal {  
    GREEN, YELLOW, RED  
}  
public class TrafficLight {  
    Signal color = Signal.RED;  
    public void change() {  
        switch (color) {  
        case RED:  
            color = Signal.GREEN;  
            break;  
        case YELLOW:  
            color = Signal.RED;  
            break;  
        case GREEN:  
            color = Signal.YELLOW;  
            break;  
        }  
    }  
} 

3. 枚举在工作中的一个应用

我们先来看这段代码吧:

我们主要是想表达一个红路灯的展示, 所以代码如下:

package com.abc.test;

import lombok.Data;

/**
 * @author lingxiangxinag
 */
@Data
public class AppStatusDTO {

    /** app的ID */
    private String appInstanceId;

    /** app的状态 */
    private Status status;

    /** 状态的内容 */
    private String content;

    /** 时间戳 */
    private Integer timestamp;

    // red, yellow, green, unknown代表的就是枚举出来的不同实例,传入的参数, 后面的构造函数中Status(int i)中进行的定义
  
    public enum Status {
        /** 不健康 */
        red(2),
        /** 良好 */
        yellow(1),
        /** 健康 */
        green(0),
        /** 未知,  最后一个一定是";", 其他的都是",", ";"代表枚举结束 */
        unknown(-1);

        public final int num;

        private Status(int i) {
            this.num = i;
        }
        // 自定义方法, 根据num获取枚举
        public static Status valueOf(int num){
            switch (num){
                case 0 : return AppStatusDTO.Status.green;
                case 1: return AppStatusDTO.Status.yellow;
                case 2: return AppStatusDTO.Status.red;
                default:return AppStatusDTO.Status.unknown;
            }
        }
    }

}

其实我们可以这样理解, 我们把2代表是红色, 1代表是黄色, 0代表的绿色, -1代表是-1;

其实, 我个人理解, 如果枚举是有参数的, 一个或者以上, 都应给应该他内部的构造方法, 下面列举两个参数的枚举

public enum Color {  
    RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);  
    // 成员变量  
    private String name;  
    private int index;  
    // 构造方法  
    private Color(String name, int index) {  
        this.name = name;  
        this.index = index;  
    }  
    //覆盖方法  
    @Override  
    public String toString() {  
        return this.index+"_"+this.name;  
    }  
}  

有没有发现, 我们有成员变量name和index, 那我们就有气构造方法, 本人认为这个是要一一对应上的, 方便以后我们后面的使用.不过也看你代码的需要啊, 好的, 那我在返回上面的代码中:

3.1 更改枚举的属性

那我现在问一个问题, 如果我们现在要给AppStatusDTO设置一下Status的属性, 应该如何设置呢, 获取呢?

package javatest;

import org.junit.Test;

import java.util.EnumMap;
import java.util.EnumSet;

public class STest {

    @Test
    public void hello() {

        AppStatusDTO appStatusDTO = new AppStatusDTO();
        // 设置Status的变量, 这里我们直接设计为red就好, red就包含所有的值
        appStatusDTO.setStatus(AppStatusDTO.Status.red);
        // 获取Status的具体的值
        System.out.println(appStatusDTO.getStatus().num);

    }
}

3.2 获取枚举的所有类型

我们可以通过枚举的values的方法, 来遍历所有的枚举类型, 也可以通过EnumSet来遍历所有的枚举

代码如下:

package javatest;

import org.junit.Test;

import java.util.EnumMap;
import java.util.EnumSet;

public class STest {

    @Test
    public void hello() {
      // 通过values的方法来遍历所有的枚举类型
        for (AppStatusDTO.Status status: AppStatusDTO.Status.values()) {
            System.out.println(status.num);
            System.out.println(status.toString());
        }
      
      // 通过EnumSet来遍历所有的枚举
        EnumSet<AppStatusDTO.Status> colors = EnumSet.allOf(AppStatusDTO.Status.class);
        for (AppStatusDTO.Status color: colors) {
            System.out.println(color);
        }

    }
}

3.3 根据枚举某一个属性的值获取该枚举

像我们上面的代码中, valueOf就是通过属性获取枚举的方法, 可以参考, 你也可以通过下面的方法实现:

静态代码块,在虚拟机加载类的时候就会加载执行,而且只执行一次;, 今天新get到的一个知识点.

public enum WorkFlowCodeTwEnum {
    HB("华北","huabei"),
    HN("华南","huanan"),
    HZ("华中","huazhong"),
    XN("西南","xinan"),
    HD("华东","huadong");

    private String code;
    private String name;
}
private static final Map<String,WorkFlowCodeTwEnum> typeMap = Maps.newHashMap();
    static {
        for(WorkFlowCodeTwEnum s : EnumSet.allOf(WorkFlowCodeTwEnum.class)){
            typeMap.put(s.getCode(), s);
        }
    }

    public static WorkFlowCodeTwEnum getEnum(String code) {
        return typeMap.get(code);
    }

4. 使用枚举做线程安全的单例

大佬和<<Effective Java>>都推荐使用枚举做单例模式

// 定义单例模式中需要完成的代码逻辑
public interface MySingleton {
    void doSomething();
}

public enum Singleton implements MySingleton {
    INSTANCE {
        @Override
        public void doSomething() {
            System.out.println("complete singleton");
        }
    };

    public static MySingleton getInstance() {
        return Singleton.INSTANCE;
    }
}

首先, 我们先来看INSTANCE, 我们先来看这个, 这个并不是什么关键字, 就是我们工作中常用到的枚举的值, 类似于我们的 RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);等, 这个让我这个菜逼在刚看这个代码的时候一脸懵逼.

如果想了解清楚, 请查看这篇文章, 讲的是比较清楚的:https://juejin.im/post/5b285d236fb9a00e9b39fdd2, 但是我也来带着打击分析一下吧:

回答者引用了Joshua Bloch大神在《Effective Java》中明确表达过的观点:

使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

双重校验锁”实现单例:

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}  

枚举实现单例:

public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}  

相比之下, 你就会觉得枚举实现单例, 简单的多

上面的双重锁校验的代码之所以很臃肿,是因为大部分代码都是在保证线程安全。为了在保证线程安全和锁粒度之间做权衡,代码难免会写的复杂些。但是,这段代码还是有问题的,因为他无法解决反序列化会破坏单例的问题。

4.1 枚举可解决线程安全问题

上面提到过。使用非枚举的方式实现单例,都要自己来保证线程安全,所以,这就导致其他方法必然是比较臃肿的。那么,为什么使用枚举就不需要解决线程安全问题呢?

其实,并不是使用枚举就不需要保证线程安全,只不过线程安全的保证不需要我们关心而已。也就是说,其实在“底层”还是做了线程安全方面的保证的。

那么,“底层”到底指的是什么?

这就要说到关于枚举的实现了。这部分内容可以参考我的另外一篇博文深度分析Java的枚举类型—-枚举的线程安全性及序列化问题,这里我简单说明一下:

定义枚举时使用enum和class一样,是Java中的一个关键字。就像class对应用一个Class类一样,enum也对应有一个Enum类。

通过将定义好的枚举反编译,我们就能发现,其实枚举在经过javac的编译之后,会被转换成形如public final class T extends Enum的定义。

而且,枚举中的各个枚举项同时通过static来定义的。如:

public enum T {
    SPRING,SUMMER,AUTUMN,WINTER;
}

反编译后代码为:

public final class T extends Enum
{
    //省略部分内容
    public static final T SPRING;
    public static final T SUMMER;
    public static final T AUTUMN;
    public static final T WINTER;
    private static final T ENUM$VALUES[];
    static
    {
        SPRING = new T("SPRING", 0);
        SUMMER = new T("SUMMER", 1);
        AUTUMN = new T("AUTUMN", 2);
        WINTER = new T("WINTER", 3);
        ENUM$VALUES = (new T[] {
            SPRING, SUMMER, AUTUMN, WINTER
        });
    }
}

了解JVM的类加载机制的朋友应该对这部分比较清楚。static类型的属性会在类被加载之后被初始化,我们在深度分析Java的ClassLoader机制(源码级别)和Java类的加载、链接和初始化两个文章中分别介绍过,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)。所以,创建一个enum类型是线程安全的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值