本系列文章将整理到我在 GitHub 上的《Java 面试指南》仓库, 更多精彩内容请到我的仓库里查看
https://github.com/h2pl/Java-Tutorial
喜欢的话麻烦点下 Star 哈
www.how2playlife.com
枚举 (enum) 类型是 Java 5 新增的特性, 它是一种新的类型, 允许用常量来表示特定的数据片断, 而且全部都以类型安全的形式来表示.
## 初探枚举类
在程序设计中, 有时会用到由若干个有限数据元素组成的集合, 如一周内的星期一到星期日七个数据元素组成的集合, 由三种颜色红, 黄, 绿组成的集合, 一个工作班组内十个职工组成的集合等等, 程序中某个变量取值仅限于集合中的元素. 此时, 可将这些数据集合定义为枚举类型.
因此, 枚举类型是某类数据可能取值的集合, 如一周内星期可能取值的集合为:
{ Sun,Mon,Tue,Wed,Thu,Fri,Sat}
该集合可定义为描述星期的枚举类型, 该枚举类型共有七个元素, 因而用枚举类型定义的枚举变量只能取集合中的某一元素值. 由于枚举类型是导出数据类型, 因此, 必须先定义枚举类型, 然后再用枚举类型定义枚举型变量.
enum <枚举类型名>
{ <枚举元素表> };
其中: 关键词 enum 表示定义的是枚举类型, 枚举类型名由标识符组成, 而枚举元素表由枚举元素或枚举常量组成. 例如:
- enum weekdays
- { Sun,Mon,Tue,Wed,Thu,Fri,Sat };
定义了一个名为 weekdays 的枚举类型, 它包含七个元素: Sun,Mon,Tue,Wed,Thu,Fri,Sat.
在编译器编译程序时, 给枚举类型中的每一个元素指定一个整型常量值(也称为序号值). 若枚举类型定义中没有指定元素的整型常量值, 则整型常量值从 0 开始依次递增, 因此, weekdays 枚举类型的七个元素 Sun,Mon,Tue,Wed,Thu,Fri,Sat 对应的整型常量值分别为 0,1,2,3,4,5,6.
注意: 在定义枚举类型时, 也可指定元素对应的整型常量值.
例如, 描述逻辑值集合 {TRUE,FALSE} 的枚举类型 boolean 可定义如下:
- enum boolean
- { TRUE=1 ,FALSE=0 };
该定义规定: TRUE 的值为 1, 而 FALSE 的值为 0.
而描述颜色集合 {red,blue,green,black,white,yellow} 的枚举类型 colors 可定义如下:
- enum colors
- {red=5,blue=1,green,black,white,yellow};
该定义规定 red 为 5 ,blue 为 1, 其后元素值从 2 开始递增加 1.green,black,white,yellow 的值依次为 2,3,4,5.
此时, 整数 5 将用于表示二种颜色 red 与 yellow. 通常两个不同元素取相同的整数值是没有意义的. 枚举类型的定义只是定义了一个新的数据类型, 只有用枚举类型定义枚举变量才能使用这种数据类型.
### 枚举类 - 语法
enum 与 class,interface 具有相同地位;
可以继承多个接口;
可以拥有构造器, 成员方法, 成员变量;
1.2 枚举类与普通类不同之处
默认继承 java.lang.Enum 类, 所以不能继承其他父类; 其中 java.lang.Enum 类实现了 java.lang.Serializable 和 java.lang.Comparable 接口;
使用 enum 定义, 默认使用 final 修饰, 因此不能派生子类;
构造器默认使用 private 修饰, 且只能使用 private 修饰;
枚举类所有实例必须在第一行给出, 默认添加 public static final 修饰, 否则无法产生实例;
枚举类的具体使用
这部分内容参考 https://blog.csdn.net/qq_27093465/article/details/52180865
常量
public class 常量 {
- }
- enum Color {
- Red, Green, Blue, Yellow
- }
- switch
JDK1.6 之前的 switch 语句只支持 int,char,enum 类型, 使用枚举, 能让我们的代码可读性更强.
- public static void showColor(Color color) {
- switch (color) {
- case Red:
- System.out.println(color);
- break;
- case Blue:
- System.out.println(color);
- break;
- case Yellow:
- System.out.println(color);
- break;
- case Green:
- System.out.println(color);
- break;
- }
- }
向枚举中添加新方法
如果打算自定义自己的方法, 那么必须在 enum 实例序列的最后添加一个分号. 而且 Java 要求必须先定义 enum 实例.
- enum Color {
- // 每个颜色都是枚举类的一个实例, 并且构造方法要和枚举类的格式相符合.
- // 如果实例后面有其他内容, 实例序列结束时要加分号.
- Red("红色", 1), Green("绿色", 2), Blue("蓝色", 3), Yellow("黄色", 4);
- String name;
- int index;
- Color(String name, int index) {
- this.name = name;
- this.index = index;
- }
- public void showAllColors() {
- //values 是 Color 实例的数组, 在通过 index 和 name 可以获取对应的值.
- for (Color color : Color.values()) {
- System.out.println(color.index + ":" + color.name);
- }
- }
- }
覆盖枚举的方法
所有枚举类都继承自 Enum 类, 所以可以重写该类的方法
下面给出一个 toString()方法覆盖的例子.
- @Override
- public String toString() {
- return this.index + ":" + this.name;
- }
实现接口
所有的枚举都继承自 java.lang.Enum 类. 由于 Java 不支持多继承, 所以枚举对象不能再继承其他类.
- enum Color implements Print{
- @Override
- public void print() {
- System.out.println(this.name);
- }
- }
使用接口组织枚举
搞个实现接口, 来组织枚举, 简单讲, 就是分类吧. 如果大量使用枚举的话, 这么干, 在写代码的时候, 就很方便调用啦.
public class 用接口组织枚举 {
- public static void main(String[] args) {
- Food cf = chineseFood.dumpling;
- Food jf = Food.JapaneseFood.fishpiece;
- for (Food food : chineseFood.values()) {
- System.out.println(food);
- }
- for (Food food : Food.JapaneseFood.values()) {
- System.out.println(food);
- }
- }
- }
- interface Food {
- enum JapaneseFood implements Food {
- suse, fishpiece
- }
- }
- enum chineseFood implements Food {
- dumpling, tofu
- }
枚举类集合
java.util.EnumSet 和 java.util.EnumMap 是两个枚举集合. EnumSet 保证集合中的元素不重复; EnumMap 中的 key 是 enum 类型, 而 value 则可以是任意类型.
EnumSet 在 JDK 中没有找到实现类, 这里写一个 EnumMap 的例子
public class 枚举类集合 {
- public static void main(String[] args) {
- EnumMap<Color, String> map = new EnumMap<Color, String>(Color.class);
- map.put(Color.Blue, "Blue");
- map.put(Color.Yellow, "Yellow");
- map.put(Color.Red, "Red");
- System.out.println(map.get(Color.Red));
- }
- }
使用枚举类的注意事项
枚举类型对象之间的值比较, 是可以使用 ==, 直接来比较值, 是否相等的, 不是必须使用 equals 方法的哟.
因为枚举类 Enum 已经重写了 equals 方法
- /**
- * Returns true if the specified object is equal to this
- * enum constant.
- *
- * @param other the object to be compared for equality with this object.
- * @return true if the specified object is equal to this
- * enum constant.
- */
- public final boolean equals(Object other) {
- return this==other;
- }
枚举类的实现原理
这部分参考 https://blog.csdn.net/mhmyqn/article/details/48087247
Java 从 JDK1.5 开始支持枚举, 也就是说, Java 一开始是不支持枚举的, 就像泛型一样, 都是 JDK1.5 才加入的新特性. 通常一个特性如果在一开始没有提供, 在语言发展后期才添加, 会遇到一个问题, 就是向后兼容性的问题.
像 Java 在 1.5 中引入的很多特性, 为了向后兼容, 编译器会帮我们写的源代码做很多事情, 比如泛型为什么会擦除类型, 为什么会生成桥接方法, foreach 迭代, 自动装箱 / 拆箱等, 这有个术语叫 "语法糖", 而编译器的特殊处理叫 "解语法糖". 那么像枚举也是在 JDK1.5 中才引入的, 又是怎么实现的呢?
Java 在 1.5 中添加了 java.lang.Enum 抽象类, 它是所有枚举类型基类. 提供了一些基础属性和基础方法. 同时, 对把枚举用作 Set 和 Map 也提供了支持, 即 java.util.EnumSet 和 java.util.EnumMap.
接下来定义一个简单的枚举类
- public enum Day {
- MONDAY {
- @Override
- void say() {
- System.out.println("MONDAY");
- }
- }
- , TUESDAY {
- @Override
- void say() {
- System.out.println("TUESDAY");
- }
- }, FRIDAY("work"){
- @Override
- void say() {
- System.out.println("FRIDAY");
- }
- }, SUNDAY("free"){
- @Override
- void say() {
- System.out.println("SUNDAY");
- }
- };
- String work;
- // 没有构造参数时, 每个实例可以看做常量.
- // 使用构造参数时, 每个实例都会变得不一样, 可以看做不同的类型, 所以编译后会生成实例个数对应的 class.
- private Day(String work) {
- this.work = work;
- }
- private Day() {
- }
- // 枚举实例必须实现枚举类中的抽象方法
- abstract void say ();
- }
反编译结果
D:\MyTech\out\production\MyTech\com\javase \ 枚举类 > javap Day.class
Compiled from "Day.java"
public abstract class com.javase. 枚举类. Day extends java.lang.Enum<com.javase. 枚举类. Day> {
public static final com.javase. 枚举类. Day MONDAY;
public static final com.javase. 枚举类. Day TUESDAY;
public static final com.javase. 枚举类. Day FRIDAY;
public static final com.javase. 枚举类. Day SUNDAY;
java.lang.String work;
public static com.javase. 枚举类. Day[] values();
public static com.javase. 枚举类. Day valueOf(java.lang.String);
abstract void say();
com.javase. 枚举类. Day(java.lang.String, int, com.javase. 枚举类. Day$1);
com.javase. 枚举类. Day(java.lang.String, int, java.lang.String, com.javase. 枚举类. Day$1);
static {};
}
可以看到, 一个枚举在经过编译器编译过后, 变成了一个抽象类, 它继承了 java.lang.Enum; 而枚举中定义的枚举常量, 变成了相应的 public static final 属性, 而且其类型就抽象类的类型, 名字就是枚举常量的名字.
同时我们可以在 Operator.class 的相同路径下看到四个内部类的. class 文件 com/mikan/Day$1.class,com/mikan/Day$2.class,com/mikan/Day$3.class,com/mikan/Day$4.class, 也就是说这四个命名字段分别使用了内部类来实现的; 同时添加了两个方法 values()和 valueOf(String); 我们定义的构造方法本来只有一个参数, 但却变成了三个参数; 同时还生成了一个静态代码块. 这些具体的内容接下来仔细看看.
下面分析一下字节码中的各部分, 其中:
- InnerClasses:
- static #23; //class com/javase / 枚举类 / Day$4
- static #18; //class com/javase / 枚举类 / Day$3
- static #14; //class com/javase / 枚举类 / Day$2
- static #10; //class com/javase / 枚举类 / Day$1
从中可以看到它有 4 个内部类, 这四个内部类的详细信息后面会分析.
- static {};
- descriptor: ()V
- flags: ACC_STATIC
- Code:
- stack=5, locals=0, args_size=0
- 0: new #10 // class com/javase / 枚举类 / Day$1
- 3: dup
- 4: ldc #11 // String MONDAY
- 6: iconst_0
- 7: invokespecial #12 // Method com/javase / 枚举类 / Day$1."<init>":(Ljava/lang/String;I)V
- 10: putstatic #13 // Field MONDAY:Lcom/javase / 枚举类 / Day;
- 13: new #14 // class com/javase / 枚举类 / Day$2
- 16: dup
- 17: ldc #15 // String TUESDAY
- 19: iconst_1
- 20: invokespecial #16 // Method com/javase / 枚举类 / Day$2."<init>":(Ljava/lang/String;I)V
- // 后面类似, 这里省略
- }
其实编译器生成的这个静态代码块做了如下工作: 分别设置生成的四个公共静态常量字段的值, 同时编译器还生成了一个静态字段 $VALUES, 保存的是枚举类型定义的所有枚举常量
编译器添加的 values 方法:
- public static com.javase.Day[] values();
- flags: ACC_PUBLIC, ACC_STATIC
- Code:
- stack=1, locals=0, args_size=0
- 0: getstatic #2 // Field $VALUES:[Lcom/javase/Day;
- 3: invokevirtual #3 // Method "[Lcom/mikan/Day;".clone:()Ljava/lang/Object;
- 6: checkcast #4 // class "[Lcom/javase/Day;"
- 9: areturn
这个方法是一个公共的静态方法, 所以我们可以直接调用该方法 (Day.values()), 返回这个枚举值的数组, 另外, 这个方法的实现是, 克隆在静态代码块中初始化的 $VALUES 字段的值, 并把类型强转成 Day[] 类型返回.
造方法为什么增加了两个参数?
有一个问题, 构造方法我们明明只定义了一个参数, 为什么生成的构造方法是三个参数呢?
从 Enum 类中我们可以看到, 为每个枚举都定义了两个属性, name 和 ordinal,name 表示我们定义的枚举常量的名称, 如 FRIDAY,TUESDAY, 而 ordinal 是一个顺序号, 根据定义的顺序分别赋予一个整形值, 从 0 开始. 在枚举常量初始化时, 会自动为初始化这两个字段, 设置相应的值, 所以才在构造方法中添加了两个参数. 即:
另外三个枚举常量生成的内部类基本上差不多, 这里就不重复说明了.
我们可以从 Enum 类的代码中看到, 定义的 name 和 ordinal 属性都是 final 的, 而且大部分方法也都是 final 的, 特别是 clone,readObject,writeObject 这三个方法, 这三个方法和枚举通过静态代码块来进行初始化一起.
它保证了枚举类型的不可变性, 不能通过克隆, 不能通过序列化和反序列化来复制枚举, 这能保证一个枚举常量只是一个实例, 即是单例的, 所以在 effective java 中推荐使用枚举来实现单例.
枚举类实战
实战一无参
(1)定义一个无参枚举类
- enum SeasonType {
- SPRING, SUMMER, AUTUMN, WINTER
- }
(2)实战中的使用
- // 根据实际情况选择下面的用法即可
- SeasonType springType = SeasonType.SPRING; // 输出 SPRING
- String springString = SeasonType.SPRING.toString(); // 输出 SPRING
实战二有一参
(1)定义只有一个参数的枚举类
- enum SeasonType {
- // 通过构造函数传递参数并创建实例
- SPRING("spring"),
- SUMMER("summer"),
- AUTUMN("autumn"),
- WINTER("winter");
- // 定义实例对应的参数
- private String msg;
- // 必写: 通过此构造器给枚举值创建实例
- SeasonType(String msg) {
- this.msg = msg;
- }
- // 通过此方法可以获取到对应实例的参数值
- public String getMsg() {
- return msg;
- }
- }
(2)实战中的使用
- // 当我们为某个实例类赋值的时候可使用如下方式
- String msg = SeasonType.SPRING.getMsg(); // 输出 spring
实战三有两参
(1)定义有两个参数的枚举类
- public enum Season {
- // 通过构造函数传递参数并创建实例
- SPRING(1, "spring"),
- SUMMER(2, "summer"),
- AUTUMN(3, "autumn"),
- WINTER(4, "winter");
- // 定义实例对应的参数
- private Integer key;
- private String msg;
- // 必写: 通过此构造器给枚举值创建实例
- Season(Integer key, String msg) {
- this.key = key;
- this.msg = msg;
- }
- // 很多情况, 我们可能从前端拿到的值是枚举类的 key , 然后就可以通过以下静态方法获取到对应枚举值
- public static Season valueofKey(Integer key) {
- for (Season season : Season.values()) {
- if (season.key.equals(key)) {
- return season;
- }
- }
- throw new IllegalArgumentException("No element matches" + key);
- }
- // 通过此方法可以获取到对应实例的 key 值
- public Integer getKey() {
- return key;
- }
- // 通过此方法可以获取到对应实例的 msg 值
- public String getMsg() {
- return msg;
- }
- }
(2)实战中的使用
- // 输出 key 为 1 的枚举值实例
- Season season = Season.valueofKey(1);
- // 输出 SPRING 实例对应的 key
- Integer key = Season.SPRING.getKey();
- // 输出 SPRING 实例对应的 msg
- String msg = Season.SPRING.getMsg();
枚举类总结
其实枚举类懂了其概念后, 枚举就变得相当简单了, 随手就可以写一个枚举类出来. 所以如上几个实战小例子一定要先搞清楚概念, 然后在练习几遍就 ok 了.
重要的概念, 我在这里在赘述一遍, 帮助老铁们快速掌握这块知识, 首先记住, 枚举类中的枚举值可以没有参数, 也可以有多个参数, 每一个枚举值都是一个实例;
并且还有一点很重要, 就是如果枚举值有 n 个参数, 那么构造函数中的参数值肯定有 n 个, 因为声明的每一个枚举值都会调用构造函数去创建实例, 所以参数一定是一一对应的; 既然明白了这一点, 那么我们只需要在枚举类中把这 n 个参数定义为 n 个成员变量, 然后提供对应的 get() 方法, 之后通过实例就可以随意的获取实例中的任意参数值了.
如果想让枚举类更加的好用, 就可以模仿我在实战三中的写法那样, 通过某一个参数值, 比如 key 参数值, 就能获取到其对应的枚举值, 然后想要什么值, 就 get 什么值就好了.
枚举 API
我们使用 enum 定义的枚举类都是继承 java.lang.Enum 类的, 那么就会继承其 API , 常用的 API 如下:
String name()
获取枚举名称
int ordinal()
获取枚举的位置(下标, 初始值为 0 )
valueof(String msg)
通过 msg 获取其对应的枚举类型.(比如实战二中的枚举类或其它枚举类都行, 只要使用得当都可以使用此方法)
values()
获取枚举类中的所有枚举值(比如在实战三中就使用到了)
总结
枚举本质上是通过普通的类来实现的, 只是编译器为我们进行了处理. 每个枚举类型都继承自 java.lang.Enum, 并自动添加了 values 和 valueOf 方法.
而每个枚举常量是一个静态常量字段, 使用内部类实现, 该内部类继承了枚举类. 所有枚举常量都通过静态代码块来进行初始化, 即在类加载期间就初始化.
另外通过把 clone,readObject,writeObject 这三个方法定义为 final 的, 同时实现是抛出相应的异常. 这样保证了每个枚举类型及枚举常量都是不可变的. 可以利用枚举的这两个特性来实现线程安全的单例.
参考文章
- https://blog.csdn.net/qq_34988624/article/details/86592229
- https://www.meiwen.com.cn/subject/slhvhqtx.html
- https://blog.csdn.net/qq_34988624/article/details/86592229
- https://segmentfault.com/a/1190000012220863
- https://my.oschina.net/wuxinshui/blog/1511484
- https://blog.csdn.net/hukailee/article/details/81107412
微信公众号
Java 技术江湖
如果大家想要实时关注我更新的文章以及分享的干货的话, 可以关注我的公众号[Java 技术江湖] 一位阿里 Java 工程师的技术小站, 作者黄小斜, 专注 Java 相关技术: SSM,SpringBoot,MySQL, 分布式, 中间件, 集群, Linux, 网络, 多线程, 偶尔讲点 Docker,ELK, 同时也分享技术干货和学习经验, 致力于 Java 全栈开发!
Java 工程师必备学习资源: 一些 Java 工程师常用学习资源, 关注公众号后, 后台回复关键字 "Java" 即可免费无套路获取.
个人公众号: 黄小斜
作者是 985 硕士, 蚂蚁金服 JAVA 工程师, 专注于 JAVA 后端技术栈: SpringBoot,MySQL, 分布式, 中间件, 微服务, 同时也懂点投资理财, 偶尔讲点算法和计算机理论基础, 坚持学习和写作, 相信终身学习的力量!
程序员 3T 技术学习资源: 一些程序员学习技术的资源大礼包, 关注公众号后, 后台回复关键字 "资料" 即可免费无套路获取.
来源: https://www.cnblogs.com/xll1025/p/11630652.html