枚举

87 阅读7分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情

枚举

枚举类型是java5引入的,由一组固定常量组成的合法类型。

在枚举引入之前如何定义一组常量

java在枚举引入之前,我们一般会用一组int常量值,来表示一组固定的数据。比如使用1、2、3、4来表示春、夏、秋、冬。

/**
 * 枚举类型一般会被系统共享,所以其访问修饰符一般为public
 */
class Season {
    public static final int SPRING = 1;
    public static final int SUMMER = 2;
    public static final int AUTUMN = 3;
    public static final int WINTER = 4;
}

可以根据传入的int值来判断对应季节

@Test
public void test1() {
    final int spring = Season.SPRING;
    season(spring);
}
public void season(int value) {
    switch (value) {
        case 1:
            System.out.println("春天");
            break;
        case 2:
            System.out.println("夏天");
            break;
        case 3:
            System.out.println("秋天");
            break;
        case 4:
            System.out.println("冬天");
            break;
        default:
            System.out.println("输入不合法");
            break;
    }
}

这种方法称作int枚举模式。存在一些安全问题,就如上面判断季节的方法,default分支是我们不愿意看到的场景,如果说我们不加校验可能会产生问题。并且Season这个类打印出来的也只是一个int值1、2、3、4,表面并不能看出任何的意思。所以说int枚举模式他的安全性和可读性是不可观的。

当然了我们也可以使用字符串作为枚举值,但是字符串的比较算法相对来说比较浪费性能,也是不可取的。

定义枚举

由于int枚举和字符串枚举存在着缺陷,java5引入了枚举类型enum type,接下来我们看如何定义一个枚举。

使用enum声明一个枚举,在枚举类中列举枚举值,使用逗号隔开,尾部使用分号结尾。

enum Season2 {
    SPRING, SUMMER, AUTUMN, WINTER;  
}

并且我们还可以为枚举定义属性:

@AllArgsConstructor
enum Season3 {
    SPRING(1, "春天"),
    SUMMER(1, "春天"),
    AUTUMN(1, "春天"),
    WINTER(1, "春天");
    int code;
    String msg;
}
特点
  • 简约
  • 和普通class类一样,枚举类可以单独存在,也可以存在于其他java类中
  • 枚举类可以实现接口
  • 也可以定义新的属性和方法
switch对于枚举的支持

使用枚举改造上面代码

public void seasonUseEnum(Season2 season) {
    System.out.println(Season2.SPRING);
    switch (season) {
        case SPRING:
            System.out.println("春天");
            break;
        case SUMMER:
            System.out.println("夏天");
            break;
        case AUTUMN:
            System.out.println("秋天");
            break;
        case WINTER:
            System.out.println("冬天");
            break;
        default:
            System.out.println("输入不合法");
            break;
    }
}

@Test
public void test2() {
    seasonUseEnum(Season2.SPRING);
}

如此判断季节的方法对于传入参数存在类型限制,不会再有不合法参数的出现。一般来说我们会对枚举添加表示域的属性和对应的描述,方便统一管理。

public void seasonUseEnum(Season3 season) {
    System.out.println(Season2.SPRING);
    final StringBuilder sb = new StringBuilder();
    switch (season) {
        case SPRING:
        case WINTER:
        case AUTUMN:
        case SUMMER:
            sb.append(season.msg);
            break;
        default:
            System.out.println("输入不合法");
            break;
    }
    System.out.println(sb.toString());
}
@Test
public void test3() {
    seasonUseEnum(Season3.SPRING);
}
jad查看原理

可以使用jad 反编译一下,查看一下底层原理

可以得出如下结论:

  • 枚举类经过编译器编译后会被当作普通类处理,继承自 java.lang.Enum
  • 每一个枚举项是一个 final static的成员变量。天生是一个单例
final class Season3 extends Enum
{
    private Season3(String s, int i, int code, String msg)
    {
        super(s, i);
        this.code = code;
        this.msg = msg;
    }
    public static final Season3 SPRING;
    public static final Season3 SUMMER;
    public static final Season3 AUTUMN;
    public static final Season3 WINTER;
    int code;
    String msg;
    private static final Season3 $VALUES[];
    static 
    {
        SPRING = new Season3("SPRING", 0, 1, "\u6625\u5929");
        SUMMER = new Season3("SUMMER", 1, 1, "\u6625\u5929");
        AUTUMN = new Season3("AUTUMN", 2, 1, "\u6625\u5929");
        WINTER = new Season3("WINTER", 3, 1, "\u6625\u5929");
        $VALUES = (new Season3[] {
            SPRING, SUMMER, AUTUMN, WINTER
        });
    }
}

但是要想知道switch对枚举的支持的原理,其实就在构造函数内,会调用super(s,i)。s是String类型为枚举项的字段名称,i为自动生成的编号。

我们使用jad对switch相关代码反编译一下:

  • 首先枚举类中的每一个枚举都是一个单例对象,在使用new 关键字创建实例的时候会为各个实例添加一个编号 ordinal
  • 在引用了枚举类的类中,会在static代码块中初始化一个int类型的数组,用于描述各个枚举值对应的编号
  • switch还是对int做操作
 {
     static final int $SwitchMap$com$roily$booknode$javatogod$_01faceobj$javakeywords$aboutenum$Season3[];
     static 
     {
         $SwitchMap$com$roily$booknode$javatogod$_01faceobj$javakeywords$aboutenum$Season3 = new int[Season3.values().length];
         $SwitchMap$com$roily$booknode$javatogod$_01faceobj$javakeywords$aboutenum$Season3[Season3.SPRING.ordinal()] int= 1;
         $SwitchMap$com$roily$booknode$javatogod$_01faceobj$javakeywords$aboutenum$Season3[Season3.WINTER.ordinal()] = 2;
         $SwitchMap$com$roily$booknode$javatogod$_01faceobj$javakeywords$aboutenum$Season3[Season3.AUTUMN.ordinal()] = 3;
         $SwitchMap$com$roily$booknode$javatogod$_01faceobj$javakeywords$aboutenum$Season3[Season3.SUMMER.ordinal()] = 4;
     }
 }
public void seasonUseEnum(Season3 season)
{
    System.out.println(Season2.SPRING);
    StringBuilder sb = new StringBuilder();
    switch(_cls1..SwitchMap.com.roily.booknode.javatogod._01faceobj.javakeywords.aboutenum.Season3[season.ordinal()])
    {
    case 1: // '\001'
    case 2: // '\002'
    case 3: // '\003'
    case 4: // '\004'
        sb.append(season.msg);
        break;
    default:
        System.out.println("\u8F93\u5165\u4E0D\u5408\u6CD5");
        break;
    }
}
枚举是单例的最佳实践

单例的实现方式存在很多,懒汉式、饿汉式、双重检验锁、静态内部类、枚举

单例的设计主要考虑两个问题:

  • 延时加载

    在希望使用的时候才进行单例创建,在未正真使用不创建。那么双重检验锁、静态内部类符合需求

  • 线程安全

    单例实现的复杂问题在于需要考虑线程安全问题,同时兼虑性能。懒汉式非线程安全。

1、懒汉式可实现,但非线程安全

2、饿汉式不行,饿汉式单例的创建由类加载器实现,但线程安全

3、懒汉式配合Synchronized可实现,但影响性能(会对访问单例也进行加锁操作,但访问是没有线程安全问题的)

4、双重检验锁可延时加载:是对懒汉式+锁机制的优化。避免读时加锁

5、静态内部类可实现且线程安全,也是类加载器保证的线程安全

为何枚举是单例的最佳实现?
  • 枚举天生单例,且线程安全,枚举作为内部类可实现延时加载。
  • 枚举可避免序列化、或反射 破坏单例 (枚举的序列化是定制的,序列化时会将枚举项名记录,反序列化时会根据枚举项名称找到对应枚举项)

以上编写枚举反编译查看枚举中的每一个枚举项都被final static 修饰,且在static代码块中初始化,这也就是饿汉式单例的实现。

尝试使用反射破坏单例

我们知道单例的实现,重点在于构造函数私有化,并提供获取实例的方法,那么我们就来破坏构造方法的私有性

首先写一个单例类:

public class SingleDemo implements Serializable {
    private static final long serialVersionUID = -6489201409969990006L;+
    private static SingleDemo singleDemo;
    //构造方法私有化
    private SingleDemo() {
    }
    public static SingleDemo getInstance() {
        if (null == singleDemo){
            singleDemo = new SingleDemo();
        }
        return singleDemo;
    }
}
@Test
public void testSingle() {
    final SingleDemo instance1 = SingleDemo.getInstance();
    final SingleDemo instance2 = SingleDemo.getInstance();
    System.out.println(instance1 == instance2);
}

image-20220826013226826

反射破坏单例:

final SingleDemo instance1 = SingleDemo.getInstance();
final Constructor<? extends SingleDemo> declaredConstructor = instance1.getClass().getDeclaredConstructor();
declaredConstructor.setAccessible(true);
final SingleDemo singleDemo = declaredConstructor.newInstance(null);
System.out.println(instance1 == singleDemo);

image-20220826013415047

序列化破坏单例

先执行test1,再执行test2

@Test
public void test1() {
    String filePath = "/Users/rolyfish/Desktop/MyFoot/myfoot/foot/testfile";
    final SingleDemo instance1 = SingleDemo.getInstance();
    try (final ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(new File(filePath, "object.txt")))) {
        //将instance写入文件
        objectOutputStream.writeObject(instance1);
        objectOutputStream.flush();
    } catch (IOException e) {
    }
}
@Test
public void test2() {
    String filePath = "/Users/rolyfish/Desktop/MyFoot/myfoot/foot/testfile";
    final SingleDemo instance1 = SingleDemo.getInstance();
    SingleDemo sngleDemo = null;
    try (final ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File(filePath, "object.txt")))) {
        //将instance写入文件
        sngleDemo = (SingleDemo) objectInputStream.readObject();
    } catch (IOException | ClassNotFoundException e) {
    }
    System.out.println(sngleDemo == instance1);
    System.out.println(sngleDemo);
    System.out.println(instance1);
}

image-20220826014043371

枚举可避免以上问题

尝试使用反射破坏枚举单例:枚举的构造方法除枚举自定义的还有Enum类中的code。

会报出IllegalArgumentException异常,不可以使用反射创建枚举对象。

@Test
public void test2() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    final Constructor<? extends SignalEnum> declaredConstructor = SignalEnum.SIGNAL_ENUM.getClass().getDeclaredConstructor(String.class,int.class);
    declaredConstructor.setAccessible(true);
    final SignalEnum signalEnum = declaredConstructor.newInstance("signalEnum",2);
    System.out.println(signalEnum == SignalEnum.SIGNAL_ENUM);
    System.out.println(SignalEnum.SIGNAL_ENUM);
    System.out.println(signalEnum);
}

image-20220826015207256

尝试使用序列化破坏枚举单例

先执行testx 再执行testy

结果是true,表示序列化不会破坏枚举单例。

@Test
public void testx() {
    String filePath = "/Users/rolyfish/Desktop/MyFoot/myfoot/foot/testfile";
    final SignalEnum signalEnum = SignalEnum.SIGNAL_ENUM;
    try (final ObjectOutputStream objectOutputStream = new ObjectOutputStream(
            new FileOutputStream(new File(filePath, "object2.txt")))) {
        //将instance写入文件
        objectOutputStream.writeObject(signalEnum);
        objectOutputStream.flush();
    } catch (IOException e) {
    }
}

@Test
public void testy() {
    String filePath = "/Users/rolyfish/Desktop/MyFoot/myfoot/foot/testfile";
    final SignalEnum signalEnum = SignalEnum.SIGNAL_ENUM;
    SignalEnum signalEnum2 = null;
    try (final ObjectInputStream objectInputStream = new ObjectInputStream(
            new FileInputStream(new File(filePath, "object2.txt")))) {
        //将instance写入文件
        signalEnum2 = (SignalEnum) objectInputStream.readObject();
    } catch (IOException | ClassNotFoundException e) {
    }
    System.out.println(signalEnum == signalEnum2);
    System.out.println(signalEnum);
    System.out.println(signalEnum2);
}

image-20220826015618559

原因:

枚举类型拒绝反射创建实例。

枚举类型在序列话的时候会将枚举项,对应名称序列化,而具体信息不会序列话,在反序列化时,会根据枚举项名称,调用valueOf方法返回枚举项。