你真的会用枚举吗

今天我们来聊聊枚举。你可能会想:枚举那么简单,有什么好讨论的?

没错,枚举确实是一个极为常见的知识点,常见到很多人会忽略它,而只关注它最简单的用法。当然,这和枚举诞生的初衷有关。在 JDK1.5 以前,那是个没有枚举的时代,人们通过 public static final 的常量来定义一些全局使用的标识。甚至到现在,枚举已经诞生很长时间了,但仍有一些人在使用这种方案。而且,在 C 语言中也有类似的"宏"的概念,如果你只是用来做全局标识,那么枚举的意义就没有那么大了。

但是,你真的了解枚举吗?今天我们就分别从实现原理、数据结构和设计模式 3 个方向来重新认识一下枚举。

枚举原理

我们知道一个枚举的定义非常简单。如果只考虑其作为标识的场景,那么从实现成本来看,枚举和 public static final 的传统方式差不多,甚至前者还更简单些。

arduino 复制代码
public enum Test{
  A,B
}

每一个枚举成员都可以看作是枚举类的实例,上面的 Test.A 的类型也是 Test。

ini 复制代码
Test t=Test.A;

上面这个赋值语句看上去很简单,仔细思考里面包含了几层意思。首先左边是 Test 类型的实例 t,那么右边必然是一个类的实例。但是 Test.A 看上去像是一个类,这里很容易混淆。请注意,Test.A 是一个对象,不要被这里的大写忽悠了,它不是类。

我们把这个枚举翻译成下面的样子你是不是更熟悉?

ini 复制代码
  Test A=new Test();
  Test B=new Test();

Java 枚举类型的实现是在编译阶段进行的。这个阶段和泛型的实现一样,也就是说对 JVM 来说执行的字节码集合并没有增加任何新的指令,只是在 Java 代码的层面加了一些语法。说白了,就是对已有的 JVM 指令集加了一层皮。

举个生活化的例子,"进食"是人类最基本的行为,酒店会说"用餐",但"用餐"是人体的新功能吗?并不是。在计算机界这叫"语法糖",看着很神奇,写着也很爽,但底层还是老的功能。

我们可以对 Test.class 文件进行反编译,注意反编译命令是 javap,其中-p 的意思是反编译的时候要包含私有方法。

arduino 复制代码
javap -p Test.class

输出结果为:

arduino 复制代码
public final class Test extends java.lang.Enum<Test> {
   public static final Test A;
   public static final Test B;
   public static Test[] values();
   public static Test valueOf(java.lang.String);
   private Test();
   static {};
 }

我们可以看到,确实 A 和 B 都是 Test 类的实例。Test 继承了 java.lang.Enum 类,这里还有一个 Test 的无参构造方法,这里的 A 和 B 分别使用这个构造方法来实例化。

而实例化的过程发生在哪呢?

我们注意到上面代码段的最后有一个空的 static 语句块,我们可以基于 javap 的其他参数进一步分析 static 里面的字节码内容。static 里面其实包含了很多字节码指令,正是这些指令在做 A、B 的初始化工作。而 static 代码块是在类加载的时候执行的。也就是说当 Test 被加载的时候,A、B 就被初始化了。

static 内部除了完成初始化 A、B,还创建了一个名叫 ENUM$VALUES 的数组,然后把 A、B 按照定义的顺序放入这个数组中。最后我们可以通过 values 方法来访问这个数组。

这里我们可以增加一个构造方法,这样大家就比较熟悉了。A、B 后面可以加一个括号调用这个构造方法。

arduino 复制代码
public enum Test{
  A("a"),B("b");
  Test(String name){
    this.name=name;
  }
  private String name;
}

上面 A("a") 相当于如下代码的简写:

ini 复制代码
Test A=new Test("a");

我们再把例子做的复杂一些,我们为 Test 增加一个抽象方法 print。这里就不能像上面那样直接初始化了。这里必须使用类似匿名内部类的写法。

typescript 复制代码
public enum Test{
  A("a"){
    @Override
    public void print(){
      System.out.print("a");
    }
  },
  B("b"){
    @Override
    public void print(){
      System.out.print("b");
    }
  };
  Test(String name){
    this.name=name;
  }
    private String name;
    public abstract void print();
}

上面这个写法有点不伦不类,你需要适应一下。如果你去编译目录下查看文件,这次你会发现编译后多了 Test和2.class 2 个文件,也就是匿名内部类。可见语法糖已经帮我们做好了一切。

数据结构

说完了枚举的实现原理,我们再看看它支持的一些数据结构。常见的有 EnumSet 和 EnumMap。1.EnumSet

EnumSet 显然是为枚举打造的抽象集合类。它使用了位图来存储数据,因此非常紧凑。

EnumSet 有 2 个实现类,RegularEnumSet 和 JumboEnumSet。当我们创建 EnumSet 的时候,如果枚举成员数量小于等于 64 将会使用 RegularEnumSet,大于 64 则会创建 JumboEnumSet。

为什么创建 EnumSet 的时候,会有不同的实现类呢?

这是因为 RegularEnumSet 采用 long 来存储枚举变量,而 long 是 64 位的,因此只能存储 64 个变量。而 JumboEnumSet 使用 long[]来存储枚举变量,因此没有这个限制。当然,你见过一个枚举类超过 64 个成员变量吗?如果真有这种情况,我认为放到 ZooKeeper 中会更合适一些。

这是一个很有意思的哲学问题,当你的枚举变量只有 2 个,这个枚举一般是很稳定的,但是你的枚举变量超过了 64 个,我相信随着业务发展枚举数量还会新增,这种情况下就不适合用枚举来解决了。也就是说枚举变量越多,业务越不稳定。

ini 复制代码
EnumSet支持常见的集合操作,如取子集、增加、删除、包含等。可以使用EnumSet的of方法来初始化。
EnumSet<Test> testSet = EnumSet.of(Test.A, Test.B);

2.EnumMap

EnumMap 很明显是一个 Map 结构,它的 key 就是枚举,value 可以由你定义。比如下面这个声明的意思就是对所有的 Object 按照 Test 枚举类型来分类。其结果是输出类型 A 及属于 A 类型的对象,输出类型 B 及属于 B 类型的对象。

swift 复制代码
EnumMap<Test,List<Object>> testMap;

设计模式

最后我们聊聊枚举和设计模式的关系。

单例模式有很多实现方法,其中最好的就是用枚举来实现。例如,下面的代码段:

csharp 复制代码
 public enum Singleton{
        INSTANCE;
        private Singleton(){
            //做一些初始化工作
        }
    }

上面的 INSTANCE 就是我们的单例对象,我们可以把一些初始化工作放到 Singleton 的构造方法里面。还记得前面我们说的,枚举的成员就是枚举类的实例化对象,这个过程发生在 static 语句块中。上面这段话所传达的语义类似下面这样:

ini 复制代码
Singleton INSTANCE=null;
static{
  INSTANCE = new Singleton();
}

此外枚举在序列化和反序列化的时候并不会调用构造方法,这在一定程度上保障了单例。序列化仅仅是将枚举对象的 name 属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf 方法来根据名字查找枚举对象。这里的处理和普通的类有很大的差异。

另外枚举还可以实现模板模式、策略模式等。但是注意不要把太多代码放到枚举类,这样不便于维护。关于用枚举实现其他的设计模式读者可以自己试试。

总结

上面就是枚举的核心内容。我们知道枚举本质上是一个语法糖,底层是通过继承 java.lang.Enum 来实现的。枚举的每个成员都是枚举类的实例,并且还有自己的 Set 和 Map 数据结构,通过上面的分析我们可以看出枚举底层实现很普通,但是很多语法特性超越了普通的 Java 类,在设计单例模式以及一些模板模式中将简化编码工作,使得工程整体变的更优雅、更紧凑。

相关推荐
颜如玉13 分钟前
ElasticSearch关键参数备忘
后端·elasticsearch·搜索引擎
JH307316 分钟前
Maven的三种项目打包方式——pom,jar,war的区别
java·maven·jar
带刺的坐椅1 小时前
轻量级流程编排框架,Solon Flow v3.5.0 发布
java·solon·workflow·flow·solon-flow
卡拉叽里呱啦1 小时前
缓存-变更事件捕捉、更新策略、本地缓存和热key问题
分布式·后端·缓存
David爱编程1 小时前
线程调度策略详解:时间片轮转 vs 优先级机制,面试常考!
java·后端
码事漫谈2 小时前
C++继承中的虚函数机制:从单继承到多继承的深度解析
后端
阿冲Runner2 小时前
创建一个生产可用的线程池
java·后端
哆啦code梦3 小时前
趣谈设计模式之策略模式-比特咖啡给你一杯满满的情绪价值,让您在数字世界里”畅饮“
设计模式·策略模式
喵手3 小时前
如何利用Java的Stream API提高代码的简洁度和效率?
java·后端·java ee