夯实Java基础系列14深入理解Java枚举类

40次阅读

共计 10179 个字符,预计需要花费 26 分钟才能阅读完成。

本系列文章将整理到我在 GitHub 上的《Java 面试指南》仓库,更多精彩内容请到我的仓库里查看

https://github.com/h2pl/Java-Tutorial

喜欢的话麻烦点下 Star、Fork、Watch 三连哈,感谢你的支持。

文章首发于我的个人博客:

www.how2playlife.com

本文是微信公众号【Java 技术江湖】的《夯实 Java 基础系列博文》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。

该系列博文会告诉你如何从入门到进阶,一步步地学习 Java 基础知识,并上手进行实战,接着了解每个 Java 知识点背后的实现原理,更完整地了解整个 Java 技术体系,形成自己的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。

@[toc]
如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java 技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。

<!– more –>

枚举(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_2709…

常量

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/…

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_3498…
https://www.meiwen.com.cn/sub…
https://blog.csdn.net/qq_3498…
https://segmentfault.com/a/11…
https://my.oschina.net/wuxins…
https://blog.csdn.net/hukaile…

微信公众号

Java 技术江湖

如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号【Java 技术江湖】一位阿里 Java 工程师的技术小站,作者黄小斜,专注 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点 Docker、ELK,同时也分享技术干货和学习经验,致力于 Java 全栈开发!

Java 工程师必备学习资源: 一些 Java 工程师常用学习资源,关注公众号后,后台回复关键字 “Java” 即可免费无套路获取。

个人公众号:黄小斜

作者是 985 硕士,蚂蚁金服 JAVA 工程师,专注于 JAVA 后端技术栈:SpringBoot、MySQL、分布式、中间件、微服务,同时也懂点投资理财,偶尔讲点算法和计算机理论基础,坚持学习和写作,相信终身学习的力量!

程序员 3T 技术学习资源: 一些程序员学习技术的资源大礼包,关注公众号后,后台回复关键字 “资料” 即可免费无套路获取。

正文完
 0