java枚举

1、为何使用Java枚举


Java5之前没有enum的情况下,我们一般使用几个int常量表示枚举值(int枚举类型)或者string枚举类型

public static final ZOO_CAT=1 #表示小猫

public static final ZOO_CAT=2 #表示小狗

如作为方法参数时,zooType只能是int类型

如 public void printZoo(int zooType){

}

 

这样的话 我们调用方法时只要是int类型就行 ,就没法控制方法调用的安全性

其次 作为一个数字 如果打印到日志中就是个裸数字,没有可读性

为此 java5 产生了enum , 固定且有限的类型都可以使用enum。

 

2、如何使用java枚举


 

package com.yangfei.test.meiju;

public class EnumYf {
    enum ZOO{
        CAT,DOG,PIG
    }

    public static void printZoo(ZOO zooType){
        System.out.println("my name is " + zooType);
    }

    public static void main(String[] args){
        printZoo(ZOO.DOG);
    }
}
View Code

如上 一个简单的例子 EnumYf中定义了一个动作枚举类型。

当一个枚举是公共的分类时 我们应该定义单独的文件定义,如:

public enum xxx{

  x1,x2,x3

}

 

3、枚举其实就是一个类


3.1 枚举就是一个类

定义一个enum

package com.yangfei.test.meiju;

public class EnumYf {
    enum ZOO{
        CAT,DOG,PIG
    }

    public static void printZoo(ZOO zooType){
        System.out.println("my name is " + zooType);
    }

    public static void main(String[] args){
        printZoo(ZOO.DOG);
        System.out.println(ZooEnum.CAT);
    }
}
View Code

 

对编译生成的class文件 使用javap -p -c xx.class查看生成的字节码文件:

可以看到:

  •  enum编译后 实际生成了一个class ,此类是final的且继承抽象类Enum
  • 构造方法时private 表面枚举是没法外部在new一个新对象的
  • 枚举值实际就是当前类的几个常量实例

 

所以我们使用枚举是没法再继承其他类的,但可以实现接口。

 

3.2 我们再看看Enum抽象类的结构

 

实现了Comparable和Serializable接口 表面Enum是有内在的排序和可序列化的

 

 我们看几个方法:

a. compareTo方法

 

public final int compareTo(E o) {
        Enum<?> other = (Enum<?>)o;
        Enum<E> self = this;
        if (self.getClass() != other.getClass() && // optimization
            self.getDeclaringClass() != other.getDeclaringClass())
            throw new ClassCastException();
        return self.ordinal - other.ordinal;
    }

  compareTo实际比较的是ordinal

b. ordinal方法

   直接返回ordinal属性值 这个值表示在enum中枚举值的定义顺序 从0开始。

  看java源码说明。一般用在EnumSet和EnumMap中 ,外部不要轻易使用ordinal(万一枚举定义顺序变了咋办!)

 

/**
     * The ordinal of this enumeration constant (its position
     * in the enum declaration, where the initial constant is assigned
     * an ordinal of zero).
     *
     * Most programmers will have no use for this field.  It is designed
     * for use by sophisticated enum-based data structures, such as
     * {@link java.util.EnumSet} and {@link java.util.EnumMap}.
     */
    private final int ordinal;

 

 

4、枚举常用场景


 

4.1 values方法 

   values方法会返回所有的枚举实例,数组类型。

   这个方法不是Enum类中的方法,是编译器编译中自动添加的方法。如下所示:

 

 

4.2 作为方法的入参

    如:

public class EnumYf {
    public static void printZoo(ZooEnum zooEnum){
        System.out.println(zooEnum.name());
    }
    public static void main(String[] args){
        printZoo(ZooEnum.DOG);
    }
}
View Code

 

4.3 switch判断

public class EnumYf {
    public static void printZoo(ZooEnum zooEnum){
        switch (zooEnum){
            case DOG:
                System.out.println("this is dog");
                break;
            case CAT:
                System.out.println("this is cat");
                break;
            case PIG:
                System.out.println("this is pig");
                break;
        }
    }
    public static void main(String[] args){
        printZoo(ZooEnum.DOG);
    }
}
View Code

 

 

5、枚举的序列化


枚举是可序列化的 

但枚举的序列化比较特殊,序列化时只会将枚举的name(就是一字符串)序列化

反序列时 根据枚举的valueOf(String name)找到对应的枚举常量,也就是说即使反序列化也不会生成新的实例

这点对单例模式就有绝对的保护 ,单例默认参考博主另一篇博文:https://www.cnblogs.com/yangfei629/p/11370732.html

 

6、特定于常量的方法实现

 


 

看代码:

 1 package com.yangfei.test.meiju;
 2 
 3 public enum ZooEnum {
 4     CAT("xiaomao"){
 5         @Override
 6         public void printZoo() {
 7             System.out.println("my name is xiaomao");
 8         }
 9     },
10     DOG("xoiaogou"){
11         @Override
12         public void printZoo() {
13             System.out.println("my name is xoiaogou");
14         }
15     },
16     PIG("xiaozhu"){
17         @Override
18         public void printZoo() {
19             System.out.println("my name is xiaozhu");
20         }
21     };
22 
23     private String name;
24     ZooEnum(String name) {
25         this.name = name;
26     }
27 
28     /**
29      * 抽象方法 具体实现在每个枚举中
30      */
31     public abstract void printZoo();
32 }
View Code

枚举中有抽象方法 每个枚举中有具体的实现

这么做通常用于每个枚举值有不同的实现。

同时在新增枚举值时 编译器会要求必须新增方法实现,防止漏实现。

 

7、枚举实现接口


 

 枚举是final的 即不能被继承也不能继承其他类,但可以实现接口。

 1 package com.yangfei.test.meiju;
 2 
 3 public enum ZooEnum implements ZooInterface{
 4     CAT("xiaomao"),
 5     DOG("xoiaogou"),
 6     PIG("xiaozhu");
 7 
 8     private String name;
 9     ZooEnum(String name) {
10         this.name = name;
11     }
12 
13     /**
14      * 实现接口中方法
15      * @return
16      */
17     @Override
18     public String getName() {
19         return name;
20     }
21 }
View Code

如上示例,enum可以实现接口,不仅仅接口方法,实际上可以像类一样定义任何方法和属性,因为其本质就是一个class。

 

8、EnumSet和EnumMap


 

针对枚举 JDK提供了专门的set和map , 更高效(后文详解),建议使用。

 

8.1 EnumMap使用

EnumMap的key就是枚举实例 不可以为null,但value可以为null,value可以为任何类型。

public static void main(String[] args){
        EnumMap<ZooEnum,String> enumMap = new EnumMap(ZooEnum.class);
        enumMap.put(ZooEnum.DOG,"xiaogou");
        enumMap.put(ZooEnum.CAT,"xiaomao");
        System.out.println(enumMap.get(ZooEnum.DOG));
    }
View Code

对于key为枚举类型时,使用EnumMap比HashMap更高效。因为EnumMap内部使用数组直接存取 时间复杂度为O(1)

看结构内部使用keyUniverse和vals两个数组

keyUniverse存放枚举所有的常量

vals放入枚举作为key所对应的值

数组的下标是使用枚举的oridinal(表示枚举的顺序 从0开始),这样存取记录时可以直接根据key的ordinal找到在数组中存储的位置。

/**
* All of the values comprising K. (Cached for performance.)
*/
private transient K[] keyUniverse;

/**
* Array representation of this map. The ith element is the value
* to which universe[i] is currently mapped, or null if it isn't
* mapped to anything, or NULL if it's mapped to null.
*/
private transient Object[] vals;

8.2 EnumSet的使用

EnumSet 用于存放相同枚举类型实例的集合

集中常用用法,如下

public static void main(String[] args){
        //方式一:建立空的Set 再往其中添加枚举
        EnumSet<ZooEnum> enumSet1 = EnumSet.noneOf(ZooEnum.class);
        enumSet1.add(ZooEnum.DOG);
        enumSet1.add(ZooEnum.CAT);
        System.out.println("enumSet1="+enumSet1);

        //方式二:取所有的枚举值放入set
        EnumSet<ZooEnum> enumSet2 = EnumSet.allOf(ZooEnum.class);
        System.out.println("enumSet2="+enumSet2);

        //方式三:取部门的枚举值放入set
        EnumSet<ZooEnum> enumSet3 = EnumSet.of(ZooEnum.DOG,ZooEnum.PIG);
        System.out.println("enumSet3="+enumSet3);

        enumSet3.forEach(tempEle -> System.out.println(tempEle));
    }
View Code

输出:

enumSet1=[CAT, DOG]
enumSet2=[CAT, DOG, PIG]
enumSet3=[DOG, PIG]
DOG
PIG

 

看看EnumSet的结构:

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

它是一个抽象类 有2种实现类RegularEnumSetJumboEnumSet

public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        Enum<?>[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");

        if (universe.length <= 64)
            return new RegularEnumSet<>(elementType, universe);
        else
            return new JumboEnumSet<>(elementType, universe);
    }

枚举常量个数<=64的情况:使用RegularEnumSet (一般我们使用枚举绝大多情况不会超过64)

枚举常量个数>64的情况:使用JumboEnumSet

RegularEnumSet: 使用位向量表示枚举集合

我们看add方法:

public boolean add(E e) {
        typeCheck(e);

        long oldElements = elements;
        elements |= (1L << ((Enum<?>)e).ordinal());
        return elements != oldElements;
    }

添加一个枚举就是把1L左移(移动位数=当前枚举值的ordinal值);再与当前的保持值elements求或运算。

elements值默认为0,long类型占用64位,每位用1来表示一个枚举值是否存在

如果添加第一个枚举值,则elements为如下值:

 

 如果再添加第3个枚举值,elements则为:

 

JumboEnumSet

超过了64个枚举值 则用long数组表示

如下右移6位 相等于除以64 算出数组所需长度。

 

JumboEnumSet(Class<E>elementType, Enum<?>[] universe) {
        super(elementType, universe);
        elements = new long[(universe.length + 63) >>> 6];
    }

还是看add方法:

public boolean add(E e) {
        typeCheck(e);

        int eOrdinal = e.ordinal();
        int eWordNum = eOrdinal >>> 6;

        long oldElements = elements[eWordNum];
        elements[eWordNum] |= (1L << eOrdinal);
        boolean result = (elements[eWordNum] != oldElements);
        if (result)
            size++;
        return result;
    }

先通过枚举的ordinal>>>6 右移6位得到的就是数组的索引下标

1L<<eOrdinal 溢出后得到的值 当做位向量处理

对于每个数组元素就是一个long类型 跟RegularEnumSet处理逻辑一致

全程都巧妙的用位运算来操作 所以比较高效。

 

注意EnumSet和EnumMap并不是线程安全的,多线程时可用锁或者Collections.synchronizedMap保护

 

posted @ 2019-08-25 00:34  蓝天随笔  阅读(1231)  评论(0编辑  收藏  举报