Java基础14-深入理解Java枚举类(一)

简介: Java基础14-深入理解Java枚举类(一)

初探枚举类

在程序设计中,有时会用到由若干个有限数据元素组成的集合,如一周内的星期一到星期日七个数据元素组成的集合,由三种颜色红、黄、绿组成的集合,一个工作班组内十个职工组成的集合等等,程序中某个变量取值仅限于集合中的元素。此时,可将这些数据集合定义为枚举类型。

因此,枚举类型是某类数据可能取值的集合,如一周内星期可能取值的集合为:

  { 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));
    }
}复制代码

使用枚举类的注意事项

image

枚举类型对象之间的值比较,是可以使用==,直接来比较值,是否相等的,不是必须使用equals方法的哟。

因为枚举类Enum已经重写了equals方法

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));
    }
}复制代码

枚举类的实现原理

这部分参考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/Day1.classcom/mikan/Day1.class、com/mikan/Day2.class、com/mikan/Day3.classcom/mikan/Day3.class、com/mikan/Day4.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中推荐使用枚举来实现单例。


Java基础14-深入理解Java枚举类(二):https://developer.aliyun.com/article/1535690

目录
相关文章
|
2天前
|
设计模式 Java 容器
在Java中调用一个接口的多个实现类
在Java中调用一个接口的多个实现类
11 4
|
23小时前
|
Java
杨老师课堂_Java教程第六篇之引用数据类型_类的运用
杨老师课堂_Java教程第六篇之引用数据类型_类的运用
5 1
|
2天前
|
Java 数据安全/隐私保护
Java基础之类封装、继承、多态
Java基础之类封装、继承、多态
8 2
|
2天前
|
安全 Java 数据安全/隐私保护
Java基础之类封装、继承、多态
Java基础的封装、继承和多态是OOP的核心。封装通过访问控制(如private)隐藏类的内部细节,提供公共接口供外部交互。例如,`Person`类封装`name`和`age`,通过`getName()`和`setAge()`方法访问。继承允许子类(如`Dog`)继承父类(如`Animal`)的属性和方法,并可扩展或覆盖。多态使得父类引用可指向子类对象,调用方法时根据实际对象类型执行,如不同动物的`makeSound()`。接口实现多态提供了一种定义行为而不必关心实现的方式。向上转型(子类→父类)安全且默认,而向下转型(父类→子类)需类型检查以避免异常。
6 1
|
2天前
|
存储 安全 Java
Java集合类是Java编程语言中用于存储和操作一组对象的工具
【6月更文挑战第19天】Java集合类,如`List`、`Set`、`Map`在`java.util`包中,提供高级数据结构。常用实现包括`ArrayList`(快速随机访问)、`LinkedList`(高效插入删除)、`HashSet`(无序不重复)、`TreeSet`(排序)、`HashMap`(键值对)和`TreeMap`(排序映射)。集合动态调整大小,支持对象引用,部分保证顺序。选择合适集合优化性能和数据组织。
8 1
|
3天前
|
安全 Java 开发者
类与对象:Java中的封装、继承与多态
Java面向对象三大特性:封装(隐藏对象细节,增强安全与复用),继承(代码复用与扩展,如Dog继承Animal),多态(统一接口,不同实现,如Playable接口的Piano和Guitar)。通过示例展示了如何在实践中应用这些概念。【6月更文挑战第16天】
12 2
|
3天前
|
Java
在 Java 中,类是一种定义对象的模板,它包含数据成员(字段)和方法。
在 Java 中,类是一种定义对象的模板,它包含数据成员(字段)和方法。
|
2天前
|
Java 大数据 API
|
1月前
|
安全 Java 调度
Java一分钟:多线程编程初步:Thread类与Runnable接口
【5月更文挑战第11天】本文介绍了Java中创建线程的两种方式:继承Thread类和实现Runnable接口,并讨论了多线程编程中的常见问题,如资源浪费、线程安全、死锁和优先级问题,提出了解决策略。示例展示了线程通信的生产者-消费者模型,强调理解和掌握线程操作对编写高效并发程序的重要性。
57 3
|
23天前
|
Java 程序员
Java中的多线程编程:理解并应用Thread类和Runnable接口
【5月更文挑战第28天】在Java中,多线程编程是一个重要的概念,它允许同时执行多个任务。本文将深入探讨Java的多线程编程,包括Thread类和Runnable接口的使用,以及如何在实际项目中应用这些知识。我们将通过实例来理解这些概念,并讨论多线程编程的优点和可能的挑战。