【小家Java】深入理解Java枚举类型(enum)及7种常见的用法(含EnumMap和EnumSet)


前文

这次当我入职一家新公司的时候,编写代码发现,里面还在大量的使用public static final…这种语句来神马一些状态常量。

很多时候,虽然都能暂时完成一样的功能,但武功高低,一看便知。因此我加入之后,迅速全面引入枚举类型,并且指定枚举的使用规范、统一实现的接口。。。

什么是枚举类型

枚举类型是Java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是因为它既是一种类(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。

枚举类型的定义

这段代码,是在enum没引入之前:

public class DayDemo {
    public static final int MONDAY =1;
    public static final int TUESDAY=2;
    public static final int WEDNESDAY=3;
    public static final int THURSDAY=4;
    public static final int FRIDAY=5;
    public static final int SATURDAY=6;
    public static final int SUNDAY=7;
}

上述的常量定义常量的方式称为int枚举模式,这样的定义方式并没有什么错,但它存在许多不足:

  1. 如在类型安全和使用方便性上并没有多少好处
  2. 如果存在定义int值相同的变量,混淆的几率还是很大的,编译器也不会提出任何警告
  3. 操作上,比如我要拿到所有的枚举值,或者根据枚举值拿到具体的名字等都非常的不方便 因此这种方式在枚举出现后并不提倡,现在我们利用枚举类型来重新定义上述的常量,同时也感受一把枚举定义的方式,如下定义周一到周日的常量
//枚举类型,使用关键字enum
public enum Day {
    MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

相当简洁,使用起来也是异常的方便(下面会着重讲解它的用法)。

枚举的真身

需要看真身,首先我们得看看编译后的.class文件。

/**
 * @author fangshixiang@vipkid.com.cn
 * @description
 * @date 2018-11-03 16:49
 */
public enum DayEnum {
    MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

查看.class文件如下:

我们发现它和普通的class文件一样,还是会生成一个同名的.class文件。现在我们反编译看看.class文件的内容:

public final class DayEnum extends Enum<DayEnum> {
    
    //编译器为我们添加的静态的values()方法
    public static DayEnum[] values() {
        return (DayEnum[])$VALUES.clone();
    }
    
    //编译器为我们添加的静态的valueOf()方法,注意间接调用了Enum也类的valueOf方法
    public static DayEnum valueOf(String s) {
        return (DayEnum)Enum.valueOf(com/fsx/run/enums/DayEnum, s);
    }
    
    //私有构造函数 只能由编译器来调用
    private DayEnum(String enumName, int index) {
        super(enumName, index);
    }
    //前面定义的7种枚举实例
    public static final DayEnum MONDAY;
    public static final DayEnum TUESDAY;
    public static final DayEnum WEDNESDAY;
    public static final DayEnum THURSDAY;
    public static final DayEnum FRIDAY;
    public static final DayEnum SATURDAY;
    public static final DayEnum SUNDAY;
    
    //装载所有实例的一个数组
    private static final DayEnum $VALUES[];

    //通过静态代码快实例这些多例
    static {
        MONDAY = new DayEnum("MONDAY", 0);
        TUESDAY = new DayEnum("TUESDAY", 1);
        WEDNESDAY = new DayEnum("WEDNESDAY", 2);
        THURSDAY = new DayEnum("THURSDAY", 3);
        FRIDAY = new DayEnum("FRIDAY", 4);
        SATURDAY = new DayEnum("SATURDAY", 5);
        SUNDAY = new DayEnum("SUNDAY", 6);
        
        //都装载进去
        $VALUES = (new DayEnum[] {
                MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
        });
    }
}

看到了enum类型编译后的真身,很多结果都一目了然了有木有。

我们注意到,DayEnum类是final类型的,将无法被继承。而且该类继承自java.lang.Enum类(它是一个抽象类,所有的enum类型的类都是它的子类,提供很多方法和定义)

这里提醒大家一点,Enum类内部会有一个构造函数,该构造函数只能有编译器调用,我们是无法手动操作的

枚举的Class对象

需求:我们需要一次性获取到所有的枚举值对象:

    public static void main(String[] args) {
        DayEnum[] values = DayEnum.values();
        DayEnum[] enumConstants = DayEnum.class.getEnumConstants();
        //判断某个class是否为枚举类型
        System.out.println(MONDAY.getClass().isEnum()); //true
    }

enum中定义抽象方法

与常规抽象类一样,enum类允许我们为其定义抽象方法,然后使每个枚举实例都实现该方法,以便产生不同的行为方式,注意abstract关键字对于枚举类来说并不是必须的如下:

public enum EnumDemo3 {

    FIRST{
        @Override
        public String getInfo() {
            return "FIRST TIME";
        }
    },
    SECOND{
        @Override
        public String getInfo() {
            return "SECOND TIME";
        }
    }

    ;

    /**
     * 定义抽象方法
     * @return
     */
    public abstract String getInfo();

    //测试
    public static void main(String[] args){
        System.out.println("F:"+EnumDemo3.FIRST.getInfo());
        System.out.println("S:"+EnumDemo3.SECOND.getInfo());
        /**
         输出结果:
         F:FIRST TIME
         S:SECOND TIME
         */
    }
}

常用的7种使用方式

用法一:常量(也是最为常用的使用场景)

在JDK1.5 之前,我们定义常量都是: public static final… 。现在好了,有了枚举,可以把相关的常量分组到一个枚举类型里,而且枚举提供了比常量更多的方法。

public enum Color {  
  // RED, GREEN, BLANK, YELLOW //若后续没有代码了,此;可以省略。否则不行
  RED, GREEN, BLANK, YELLOW;  
} 
用法二:switch

JDK1.6之前的switch语句只支持int,char,enum类型,使用枚举,能让我们的代码可读性更强。

enum Signal {  
    GREEN, YELLOW, RED  
}  
public class TrafficLight {  
    Signal color = Signal.RED;  
    public void change() {  
        switch (color) {  
        case RED:  
            color = Signal.GREEN;  
            break;  
        case YELLOW:  
            color = Signal.RED;  
            break;  
        case GREEN:  
            color = Signal.YELLOW;  
            break;  
        }  
    }  
}  
用法三:向枚举中添加新方法

如果打算自定义自己的方法,那么必须在enum实例序列的最后添加一个分号。而且 Java 要求必须先定义 enum 实例。

public enum Color {  
    RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);  
    // 成员变量  
    private String name;  
    private int index;  
    
    // 构造方法  
    private Color(String name, int index) {  
        this.name = name;  
        this.index = index;  
    }  
    
    // 普通静态方法方法  可通过枚举类直接调用
    public static String getName(int index) {  
        for (Color c : Color.values()) {  
            if (c.getIndex() == index) {  
                return c.name;  
            }  
        }  
        return null;  
    }  
    // get方法(set方法是没有必要给出的,因为并不希望改变值)  
    public String getName() {  
        return name;  
    }  
    public int getIndex() {  
        return index;  
    }  
}  
用法四:覆盖枚举的方法

下面给出一个toString()方法覆盖的例子。

public enum Color {  
    RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);  
    // 成员变量  
    private String name;  
    private int index;  
    // 构造方法  
    private Color(String name, int index) {  
        this.name = name;  
        this.index = index;  
    }  
    //覆盖方法  
    @Override  
    public String toString() {  
        return this.index+"_"+this.name;  
    }  
}  
用法五:实现接口(规范、统一控制非常有效)

所有的枚举都继承自java.lang.Enum类。由于Java 不支持多继承,所以枚举对象不能再继承其他类。

public interface Behaviour {  
    void print();  
    String getInfo();  
}  
public enum Color implements Behaviour{  
    RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);  
    // 成员变量  
    private String name;  
    private int index;  
    // 构造方法  
    private Color(String name, int index) {  
        this.name = name;  
        this.index = index;  
    }  
    
   //接口方法  
    @Override  
    public String getInfo() {  
        return this.name;  
    }  
    //接口方法  
    @Override  
    public void print() {  
        System.out.println(this.index+":"+this.name);  
    }  
}  
用法六:使用接口组织枚举

这个思想是很好的。如果你的一个模块需要有多个枚举,建议可以放在接口内,来统一组织。这样方便管理,也方便做一些多态的使用

public interface Food {  
    enum Coffee implements Food{  
        BLACK_COFFEE,DECAF_COFFEE,LATTE,CAPPUCCINO  
    }  
    enum Dessert implements Food{  
        FRUIT, CAKE, GELATO  
    }  
}  
用法七:关于枚举集合的使用(EnumSet和EnumMap)

java.util.EnumSet和java.util.EnumMap是两个枚举集合。EnumSet保证集合中的元素不重复;EnumMap中的 key是enum类型,而value则可以是任意类型

EnumMap基本用法

public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> 
			implements java.io.Serializable, Cloneable

先思考这样一个问题,现在我们有一堆size大小相同而颜色不同的数据,需要统计出每种颜色的数量是多少以便将数据录入仓库,定义如下枚举用于表示颜色Color:

enum Color {
    GREEN,RED,BLUE,YELLOW
}

显然这个如果用Map来做,估计谁都会做。但是本文采用更方便,更加高效的EnumMap来处理:

//方案2:使用EnumMap
			
		// 使用color作为key,总数Integer作为值即可方便统计
        Map<Color,Integer> enumMap=new EnumMap<>(Color.class);

        for (Clothes clothes:list){
            Color color=clothes.getColor();
            Integer count = enumMap.get(color);
            if(count!=null){
                enumMap.put(color,count+1);
            }else {
                enumMap.put(color,1);
            }
        }

        System.out.println(enumMap.toString());

EnumMap作为枚举的专属的集合,我们没有理由再去使用HashMap,毕竟EnumMap要求其Key必须为Enum类型,因而使用Color枚举实例作为key是最恰当不过了,也避免了获取name的步骤。

更重要的是EnumMap效率更高,因为其内部是通过数组实现的(具体源码分析,本文不做过多的分析)。注意EnumMap的key值不能为null,虽说是枚举专属集合,但其操作与一般的Map差不多,概括性来说EnumMap是专门为枚举类型量身定做的Map实现,虽然使用其它的Map(如HashMap)也能完成相同的功能,但是使用EnumMap会更加高效.

它只能接收同一枚举类型的实例作为键值且不能为null,由于枚举类型实例的数量相对固定并且有限,所以EnumMap使用数组来存放与枚举类型对应的值,毕竟数组是一段连续的内存空间,根据程序局部性原理,效率会相当高。

它有三个构造函数:

//创建一个具有指定键类型的空枚举映射。
EnumMap(Class<K> keyType) 
//创建一个其键类型与指定枚举映射相同的枚举映射,最初包含相同的映射关系(如果有的话)。     
EnumMap(EnumMap<K,? extends V> m) 
//创建一个枚举映射,从指定映射对其初始化。
EnumMap(Map<K,? extends V> m)  

使用实例:

//使用第一种构造
Map<Color,Integer> enumMap=new EnumMap<>(Color.class);
//使用第二种构造
Map<Color,Integer> enumMap2=new EnumMap<>(enumMap);
//使用第三种构造
Map<Color,Integer> hashMap = new HashMap<>();
hashMap.put(Color.GREEN, 2);
hashMap.put(Color.BLUE, 3);
Map<Color, Integer> enumMap = new EnumMap<>(hashMap);

EnumSet用法

public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
	    implements Cloneable, java.io.Serializable

EnumSet 中所有元素都必须是枚举类型。

与其他Set接口的实现类HashSet/TreeSet(内部都是用对应的HashMap/TreeMap实现的)不同的是,EnumSet在内部实现是位向量(稍后分析),它是一种极为高效的位运算操作。

由于直接存储和操作都是bit,因此EnumSet空间和时间性能都十分可观,足以媲美传统上基于 int 的“位标志”的运算,重要的是我们可像操作set集合一般来操作位运算,这样使用代码更简单易懂同时又具备类型安全的优势。

创建EnumSet并不能使用new关键字,因为它是个抽象类,而应该使用其提供的静态工厂方法,EnumSet的静态工厂方法比较多,如下:

// 创建一个具有指定元素类型的空EnumSet。
EnumSet<E>  noneOf(Class<E> elementType)       
//创建一个指定元素类型并包含所有枚举值的EnumSet
<E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType)
// 创建一个包括枚举值中指定范围元素的EnumSet
<E extends Enum<E>> EnumSet<E> range(E from, E to)
// 初始集合包括指定集合的补集
<E extends Enum<E>> EnumSet<E> complementOf(EnumSet<E> s)
// 创建一个包括参数中所有元素的EnumSet
<E extends Enum<E>> EnumSet<E> of(E e)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4, E e5)
<E extends Enum<E>> EnumSet<E> of(E first, E... rest)
//创建一个包含参数容器中的所有元素的EnumSet
<E extends Enum<E>> EnumSet<E> copyOf(EnumSet<E> s)
<E extends Enum<E>> EnumSet<E> copyOf(Collection<E> c)

使用示例:

enum Color {
    GREEN , RED , BLUE , BLACK , YELLOW
}

public static void main(String[] args){

        //空集合
        EnumSet<Color> enumSet= EnumSet.noneOf(Color.class);
        System.out.println("添加前:"+enumSet.toString());
        enumSet.add(Color.GREEN);
        enumSet.add(Color.RED);
        enumSet.add(Color.BLACK);
        enumSet.add(Color.BLUE);
        enumSet.add(Color.YELLOW);
        System.out.println("添加后:"+enumSet.toString());

        System.out.println("-----------------------------------");

        //使用allOf创建包含所有枚举类型的enumSet,其内部根据Class对象初始化了所有枚举实例
        EnumSet<Color> enumSet1= EnumSet.allOf(Color.class);
        System.out.println("allOf直接填充:"+enumSet1.toString());

        System.out.println("-----------------------------------");

        //初始集合包括枚举值中指定范围的元素
        EnumSet<Color> enumSet2= EnumSet.range(Color.BLACK,Color.YELLOW);
        System.out.println("指定初始化范围:"+enumSet2.toString());

        System.out.println("-----------------------------------");

        //指定补集,也就是从全部枚举类型中去除参数集合中的元素,如下去掉上述enumSet2的元素
        EnumSet<Color> enumSet3= EnumSet.complementOf(enumSet2);
        System.out.println("指定补集:"+enumSet3.toString());

        System.out.println("-----------------------------------");

        //初始化时直接指定元素
        EnumSet<Color> enumSet4= EnumSet.of(Color.BLACK);
        System.out.println("指定Color.BLACK元素:"+enumSet4.toString());
        EnumSet<Color> enumSet5= EnumSet.of(Color.BLACK,Color.GREEN);
        System.out.println("指定Color.BLACK和Color.GREEN元素:"+enumSet5.toString());

        System.out.println("-----------------------------------");

        //复制enumSet5容器的数据作为初始化数据
        EnumSet<Color> enumSet6= EnumSet.copyOf(enumSet5);
        System.out.println("enumSet6:"+enumSet6.toString());

        System.out.println("-----------------------------------");

        List<Color> list = new ArrayList<Color>();
        list.add(Color.BLACK);
        list.add(Color.BLACK);//重复元素
        list.add(Color.RED);
        list.add(Color.BLUE);
        System.out.println("list:"+list.toString());

        //使用copyOf(Collection<E> c)
        EnumSet enumSet7=EnumSet.copyOf(list);
        System.out.println("enumSet7:"+enumSet7.toString());

        /**
         输出结果:
         添加前:[]
         添加后:[GREEN, RED, BLUE, BLACK, YELLOW]
         -----------------------------------
         allOf直接填充:[GREEN, RED, BLUE, BLACK, YELLOW]
         -----------------------------------
         指定初始化范围:[BLACK, YELLOW]
         -----------------------------------
         指定补集:[GREEN, RED, BLUE]
         -----------------------------------
         指定Color.BLACK元素:[BLACK]
         指定Color.BLACK和Color.GREEN元素:[GREEN, BLACK]
         -----------------------------------
         enumSet6:[GREEN, BLACK]
         -----------------------------------
         list:[BLACK, BLACK, RED, BLUE]  //普通List没有去重
         enumSet7:[RED, BLUE, BLACK] //运用此set可以方便的去重枚举
         */
    }

其实博主认为EnumSet最有价值的是其内部实现原理,采用的是位向量,它体现出来的是一种高效的数据处理方式,这点很值得我们去学习它。

因此EnumSet的内部实现原理还是值得好好学习的。但本文不做过多的讨论了。

总结:多使用枚举,枚举的好处

enum这个关键字,可以理解为跟class差不多,这也个单独的类。

一般的class可以自己new对象,想几个就几个,而这个enum关键字,他就不行,他的实例对象,只能在这个enum里面体现。也就是说,他对应的实例是有限的。这也就是枚举的好处了,限制了某些东西的范围,举个栗子:

一年四季,只能有春夏秋冬,你要是字符串表示的话,那就海了去了,但是,要用枚举类型的话,你在enum的大括号里面把所有的选项,全列出来,那么这个季节的属性,对应的值,只能在里面挑。不能有其他的。

使用枚举的一些规范推荐

  1. 枚举类名建议带上Enum后缀,枚举成员名称需要全部大写。单词间使用下划线分隔。
  2. 强制规范:所有的枚举类型成员必须要有注释,说明每个字段的用途。(一般可以使用接口进行强制规范)

枚举类型对象之间的值比较,是可以使用==,直接来比较值,是否相等的,不是必须使用equals方法的哟。 并且,强烈建议使用==,效率更高

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券