Hey, Nice to meet You. 

必有过人之节.人情有所不能忍者,匹夫见辱,拔剑而起,挺身而斗,此不足为勇也,天下有大勇者,猝然临之而不惊,无故加之而不怒.此其所挟持者甚大,而其志甚远也.          ☆☆☆所谓豪杰之士,

夯实Java基础(十六)----枚举类的使用

1、枚举类简介

枚举:指的是仅容许特定数据类型值的有限集合。例如我们日常生活中星期一到星期日这七天就是一个特定的有限集合,一年四季的春夏秋冬也同样是的,它们都是枚举。枚举和我们数学中的集合非常相似,例如我们定义一个Season={Spring、Summer、Autumn、Winner}这样的集合,当我们要从集合中取出数据时,只能从集合中取出已经存在的数据,否则是肯定获取不到的。

枚举类:顾名思义:类的对象是有限和固定的集合。枚举类是JDK1.5中才出现的新特性,它是一种特殊的数据类型,在Java中用enum关键字来修饰,enum的全称是enumeration。所有被enum修饰的类都会默认的继承自java.lang.Enum这个类,而不是Object类。有些人并不推荐使用枚举类,而有些人则推荐使用,认为使用枚举类更加的方便。这里就要说上一句仁者见仁智者见智吧。然而在实际开发中枚举类用的还是比较的多的,用来定义各种状态。

2、枚举类的定义

上面提到了枚举类是JDK1.5才有的新特性,那么在没有枚举之前是怎么来定义的?

public class SeasonDemo {
    public static final int SPRING = 0;
    public static final int SUMMER = 1;
    public static final int AUTUMN = 2;
    public static final int WINTER = 3;

    public static void main(String[] args) {
        System.out.println(SeasonDemo.SPRING);
        System.out.println(SeasonDemo.SUMMER);
        System.out.println(SeasonDemo.AUTUMN);
        System.out.println(SeasonDemo.WINTER);
        System.out.println("---------------");
        show(SeasonDemo.SPRING);
        show(123);
    }

    public static void show(int season) {
        System.out.println(season);
    }
}

上述在没有枚举类之前我们使用public static final修饰的全局常量来表示枚举,虽然这样可以达到效果,但是也会存在缺陷。

  1. 类型不安全1:上述如果存在定义int值相同的变量的情况,那么混淆的几率还是很大的,而编译器不会提出任何警告,所以枚举类出现后基本上就不再推荐这种写法了。
  2. 类型不安全2:若一个方法中要求传入season这个参数,使用常量的话,形参就是int类型,而开发者可以传入任何int类型的值,但是如果是枚举类型的话,就只能传入枚举类中包含的对象。
  3. 代码复杂:如果我们定义时是这样定义常量的public static final int SPRING=1。而我们要的结果是输出SPRING这个字符串,那么我们还要在方法中进行大量if else 判断,如下所示。
if (season==0){
    System.out.println("SPRING");
}
......

上面大致分析了一下在没有枚举类之前是如何来定义伪枚举的,并且总结有这种方式的缺陷。而上面的问题在枚举类出现之后也算得到了解决。所以话不多说,直接上代码示例来感受一下吧:

//枚举类型,使用关键字enum
public enum Season {
    SPRING, SUMMER, AUTUMN, WINNER;
}

从上面的代码可以发现,使用枚举后代码量减少了很多,相当简洁。所以使用枚举类的好处:枚举类会让代码更加直观,类型更安全,代码量更少。但是要注意:在枚举类中声明枚举变量时,最前面不能含有任何变量,方法,构造器等内容,也就是说枚举变量必须声明在第一行。多个对象之间需要用逗号隔开,如果枚举变量中没有参数,那么最后的分号可以省略。最后再来看看枚举类的简单使用:

//枚举类型,使用关键字enum
public enum Season {
    SPRING, SUMMER, AUTUMN, WINNER;

    public static void main(String[] args) {
        Season spring = Season.SPRING;
        System.out.println(spring);
        show(Season.SUMMER);
        //show("enum"); 编译时就会报错
    }

    public static void show(Season season) {
        System.out.println(season);
    }
}

枚举的使用非常的简单,像上述代码那样,我们可以直接使用枚举类名称 . 对象名称来引用枚举的值,这便是枚举类型的最简单的使用。

3、在switch语句中的使用

在之前的switch语法中只支持int和char类型的数据,但是随着枚举类型的出现,enum类型也可以在switch中使用了,看一下怎么在switch语句中使用。

枚举在switch中使用比较简单,只需了解有这么个情况就OK了。

//枚举类型,使用关键字enum
public enum Season {
    SPRING, SUMMER, AUTUMN, WINNER;
}

class EnumTest {
    public static void main(String[] args) {
        show(Season.SPRING);
        show(Season.SUMMER);
        show(Season.AUTUMN);
        show(Season.WINNER);
    }

    public static void show(Season season) {
        switch (season) {
            case SPRING:
                System.out.println("春天");
                break;
            case SUMMER:
                System.out.println("夏天");
                break;
            case AUTUMN:
                System.out.println("秋天");
                break;
            case WINNER:
                System.out.println("冬天");
                break;
        }
    }
}

4、枚举类中常用方法

即然所有的枚举类都是继承自java.lang.Enum这个类,那么每个枚举类都必定有可用的方法。这里只例举比较常用方法:

  • Season.valueOf(String name) 方法:返回具有指定名称的指定枚举类型的枚举常量(区分大小写),如果没有则报错。
  • Season.values()方法:返回枚举类中包括所有枚举对象的数组。该方法可以很方便地遍历枚举值。
  • 枚举类变量.name()方法:返回当前枚举类对象的名称。它和toString()方法是一模一样的。
  • 枚举类变量.ordinal()方法:返回此枚举常数的序数(其枚举声明中的位置,其中初始常数的序数为零)。
  • 枚举类变量.toString()方法:返回当前枚举类对象的名称,等同于name()方法。

常用方法的举例:

public enum Season {
    SPRING, SUMMER, AUTUMN, WINNER;
}

class EnumTest {
    public static void main(String[] args) {
        //Season.valueOf()
        Season season1 = Season.valueOf("SPRING");
        System.out.println(season1);
        System.out.println("-------------");
        //Season.values()
        Season[] values = Season.values();
        for (Season season2:values){
            System.out.println(season2);
        }
        System.out.println("-------------");
        //name()
        System.out.println(Season.SUMMER.name());
        //ordinal()
        System.out.println(Season.SUMMER.ordinal());
        //toString()
        System.out.println(Season.SUMMER.toString());
    }
}

还有一些其他的方法就不介绍了,有些应该都知道,而有些则不常用。有感兴趣的话可以自己去看看官方API或者源码,都比较的简单。

5、枚举类的原理

上面已经了解了枚举类型的定义与简单使用后,那我们定义枚举类型后,到底发生了什么呢?所以现在有必要来了解一下枚举类型的基本实现原理。实际上在使用关键字enum创建枚举类型并编译后,编译器会为我们生成一个相关的类,这个类继承了Java API中的java.lang.Enum类,也就是说通过关键字enum创建枚举类型在编译后事实上也是一个类类型而且该类继承自java.lang.Enum类。所以先来了解一下Enum的源码:

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {

   // 内部定义的属性和方法省略......
}

接下来对前面定义的Season编译生成的class文件进行反编译,可以通过:javap -p Season.class 来得到反编译后的结果:

image

我们看到,对与枚举类,有很多值的注意的点:

  • 枚举类在经过编译后确实是生成了一个扩展了java.lang.Enum的类
  • 枚举类是final的,因此我们无法再继承它了
  • 我们定义的每个枚举值都是该类中的一个成员,且成员的类型仍然是Season类型
  • 枚举类中被默认增加了许多静态方法,例如values()等

为了进一步了解每个方法中的操作,我们使用javap -p -c Season.class每个方法中的字节码:

image

由于打印的太多,所以只截取了部分,根据字节码,我们还原其操作代码,大致如下:

public final class com.thr.test.Season extends java
    .lang.Enum<com.thr.test.Season>{
    public static final com.thr.test.Season SPRING;
    public static final com.thr.test.Season SUMMER;
    public static final com.thr.test.Season AUTUMN;
    public static final com.thr.test.Season WINTER;
    
    private static final com.thr.test.Season[] $VALUES;
    
    public static com.thr.test.Season values(){
        retrun (Season[])$VALUES.clone();
    }
    public static com.thr.test.Season valueOf(String s){
        retrun (Season)Enum.valueOf(java/lang/String, s);
    }
    private com.thr.test.Season(String s, int i){
        super(s,i);
    }
    
    static {
        SPRING = new Season("SPRING", 0);
        SUMMER = new Season("SUMMER", 1);
        SUMMER = new Season("AUTUMN", 2);
        WINTER = new Season("WINTER", 3);
        	$VALUES = (new Season[] {
                SPRING, SUMMER, SUMMER, WINTER
            });
    }
}

通过这里我们可以看到,在类的static操作中,编译器帮助我们生成每个枚举值的对象。

6、给枚举变量添加自定构造器

在前面的分析中,我们都是基于简单枚举类型来定义的,并没有在枚举类中定义变量、方法、构造器等结构。实际上枚举类和普通类有着相同的特征,除了不能使用继承之外(因为编译器会自动为我们继承Enum抽象类,而Java只支持单继承),我们完全可以把枚举类当成普通类,下面来感受一下吧。

public enum Season {
    SPRING("春暖花开"),
    SUMMER("夏日炎炎"),
    AUTUMN("秋高气爽"),
    WINNER("冬暖夏凉");//此时的分号不能省略

    private String name;

    //系统默认构造器是private,而且必须是private的,用public则会报错
    Season(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

class Test{
    public static void main(String[] args) {
        for (Season season:Season.values()){
            System.out.println(season.name()+","+season.getName());
        }
    }
}

程序运行结果:

img

通过上述可知,虽然枚举类中可以普通类一样声明变量、成员方法、构造器。但是我们必须注意到以下几点:

  • 枚举类变量必须放在第一行,如果枚举类变量有构造器,那么分号必须加上,否则可以省略。
  • 枚举类的构造器必须是private的,系统会自动设置为private,我们也可以自己加上,但是不能用public等修饰符,否则会报错。

7、枚举类中定义抽象方法

枚举类即然和普通类一样,那么枚举类中就允许定义抽象类和接口,接口下一节讲。但是需要注意的是每个枚举变量都需要重写该方法,否则编译会报错。继续用上面的例子,只是在其中添加了一个抽象方法。

public enum Season {
    SPRING("春暖花开"){
        public String show(){
            return "SPRING";
        }
    },
    SUMMER("夏日炎炎"){
        public String show(){
            return "SUMMER";
        }
    },
    AUTUMN("秋高气爽"){
        public String show(){
            return "AUTUMN";
        }
    },
    WINNER("冬暖夏凉"){
        public String show(){
            return "WINNER";
        }
    };//此时的分号不能省略

    private String name;

    //系统默认构造器是private,而且必须是private的,用public则会报错
    Season(String name) {
        this.name = name;
    }

    //定义抽象方法
    public abstract String show();

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

class Test{
    public static void main(String[] args) {
        for (Season season:Season.values()){
            System.out.println(season.name()+","+season.getName()+",枚举变量方法返回值:"+season.show());
        }
    }
}

程序运行结果:

img

通过这种方式就可以轻而易举地定义每个枚举变量的不同行为方式。而且调用枚举变量中方法有两种方式,一种是枚举变量名称.方法,另一种是枚举类名.枚举变量.方法(如Season.SPRING.show());

8、枚举类实现接口

虽然枚举类不能再继承其他的类了,但是它还是能够实现接口的,举例:

interface Behaviour{
    String show();
}

public enum Season implements Behaviour{
    SPRING("春暖花开"),
    SUMMER("夏日炎炎"),
    AUTUMN("秋高气爽"),
    WINNER("冬暖夏凉");//此时的分号不能省略

    private String name;

    //系统默认构造器是private,而且必须是private的,用public则会报错
    Season(String name) {
        this.name = name;
    }

    @Override
    public String show() {
        return this.name;
    }
}

class Test{
    public static void main(String[] args) {
        for (Season season:Season.values()){
            System.out.println(season.show());
        }
    }
}

这样使用起来还是比较的简单的。在枚举类中使用接口还有一个功能就是对一组数据进行分类,此时就可以利用接口来组织枚举。比如一年四季中包含的节气:

public interface Season {
    enum Spring implements Season{
        //立春,雨水,清明
        SPRING_BEGINS,RAIN,TOMB_SWEEPING
    }
    enum Summer implements Season{
        //夏至,芒种,小暑,大暑
        SUMMER_BEGINS,GRAIN_INE_EAR,SLIGHT_HEAD,GREAT_HEAD
    }
    enum AUTUMN implements Season{
        //立秋,寒露,霜降
        AUTUMN_BEGINS,COLD_DEW,FROST
    }
    enum WINTER implements Season{
        //立冬,小雪,大雪,冬至
        WINTER_BEGINS,LIGHT_SNOW,HEAVY_SNOW,WINTER_SOLSTICE_FESTIVAL
    }
}

class Test{
    public static void main(String[] args) {
        //遍历Spring
        Season.Spring[] values = Season.Spring.values();
        for (Season season:values){
            System.out.println(season);
        }
    }
}
//运行结果:
//SPRING_BEGINS
//RAIN
//TOMB_SWEEPING

通过上面这种方式我们可以很方便地组织枚举,同时还确保了具体类型属于春夏秋冬,而春夏秋冬则属于季节Season。

9、EnumSet和EnumMap的应用

EnumSet和EnumMap这两个都是关于枚举集合方面的知识,所以暂时不深入学习,仅用示例来简单了解一下。但是要清楚一点EnumSet是保证集合中的元素不重复。而EnumMap中的 key是enum类型,而value则可以是任意类型。

9.1EnumSet

关于EnumSet先来看看EnumSet的继承体系图:

image

可以发现EnumSet实现了Set接口,所以它有Set相关的特性,它相比于HashSet,它有以下优点:

  • 消耗较少的内存
  • 效率更高,因为是位向量实现的。
  • 可以预测的遍历顺序(enum 常量的声明顺序)
  • 拒绝加 null

EnumSet就是Set的高性能实现,它的要求就是存放必须是同一枚举类型。EnumSet的常用方法:

  • allof():创建一个包含指定枚举类里所有枚举值的EnumSet集合
  • range():获取某个范围的枚举实例
  • of():创建一个包括参数中所有枚举元素的EnumSet集合
  • complementOf():初始枚举集合包括指定枚举集合的补集
/**
 * EnumSet枚举
 */
public class EnumTest {
    public static void main(String[] args) {
        
        EnumSet<Season> set1, set2, set3, set4;

        set1 = EnumSet.of(Season.SPRING, Season.SUMMER, Season.AUTUMN);
        set2 = EnumSet.complementOf(set1);
        set3 = EnumSet.allOf(Season.class);
        set4 = EnumSet.range(Season.SUMMER, Season.WINNER);

        System.out.println("Set 1: " + set1);
        System.out.println("Set 2: " + set2);
        System.out.println("Set 3: " + set3);
        System.out.println("Set 4: " + set4);
    }
}

运行结果:

image

9.2EnumMap

EnumMap的继承体系图如下:

image

显然可以发现,EnumMap也实现了Map接口,它相比于HashMap,它有以下优点:

  • 消耗较少的内存
  • 效率更高
  • 可以预测的遍历顺序
  • 拒绝 null

EnumMap就是Map的高性能实现。它的常用方法跟HashMap是一致的,唯一约束是枚举相关。

/**
 * EnumMap枚举
 */
public class EnumTest {
    public static void main(String[] args) {
        EnumMap<Season, String> map = new EnumMap<Season, String>(Season.class);
        map.put(Season.SPRING, "春暖花开");
        map.put(Season.SUMMER, "夏日炎炎");
        map.put(Season.AUTUMN, "秋高气爽");
        map.put(Season.WINNER, "冬暖夏凉");

        System.out.println(map);
        System.out.println(map.get(Season.SPRING));
        //遍历map有很多种方式,这里是map遍历的一种方式,这种方式是最快的
        for (EnumMap.Entry<Season, String> entry : map.entrySet()) {
            System.out.println(entry.getKey() + "," + entry.getValue());
        }
    }
}

程序运行结果:

image


参考资料: 深入理解Java枚举类型(enum)

posted @ 2019-08-09 17:54  唐浩荣  阅读(585)  评论(0编辑  收藏  举报