EnumSet详细讲解

https://blog.csdn.net/tugangkai/article/details/89631886css

以前介绍的Set接口的实现类HashSet/TreeSet,它们内部都是用对应的HashMap/TreeMap实现的,但EnumSet不是,它的实现与EnumMap没有任何关系,而是用极为精简和高效的位向量实现的,位向量是计算机程序中解决问题的一种经常使用方式,咱们有必要理解和掌握。java

除了实现机制,EnumSet的用法也有一些不一样。次外,EnumSet能够说是处理枚举类型数据的一把利器,在一些应用领域,它很是方便和高效。算法

下面,咱们先来看EnumSet的基本用法,而后经过一个场景来看EnumSet的应用,最后,咱们分析EnumSet的实现机制。数组

基本用法ruby

与TreeSet/HashSet不一样,EnumSet是一个抽象类,不能直接经过new新建,也就是说,相似下面代码是错误的:ui

 

EnumSet<Size> set = new EnumSet<Size>();

不过,EnumSet提供了若干静态工厂方法,能够建立EnumSet类型的对象,好比:this

public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType)

noneOf方法会建立一个指定枚举类型的EnumSet,不含任何元素。建立的EnumSet对象的实际类型是EnumSet的子类,待会咱们再分析其具体实现。spa

为方便举例,咱们定义一个表示星期几的枚举类Day,值从周一到周日,以下所示:.net

 

  1.  
    enum Day {
  2.  
    MONDAY, TUESDAY, WEDNESDAY,
  3.  
    THURSDAY, FRIDAY, SATURDAY, SUNDAY
  4.  
    }

 能够这么用noneOf方法:3d

  1.  
    Set<Day> weekend = EnumSet.noneOf(Day.class);
  2.  
    weekend.add( Day.SATURDAY);
  3.  
    weekend.add( Day.SUNDAY);
  4.  
    System.out.println(weekend);

weekend表示休息日,noneOf返回的Set为空,添加了周六和周日,因此输出为:

[SATURDAY, SUNDAY]

EnumSet还有不少其余静态工厂方法,以下所示(省略了修饰public static):

  1.  
    // 初始集合包括指定枚举类型的全部枚举值
  2.  
    <E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType)
  3.  
    // 初始集合包括枚举值中指定范围的元素
  4.  
    <E extends Enum<E>> EnumSet<E> range(E from, E to)
  5.  
    // 初始集合包括指定集合的补集
  6.  
    <E extends Enum<E>> EnumSet<E> complementOf(EnumSet<E> s)
  7.  
    // 初始集合包括参数中的全部元素
  8.  
    <E extends Enum<E>> EnumSet<E> of(E e)
  9.  
    <E extends Enum<E>> EnumSet<E> of(E e1, E e2)
  10.  
    <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3)
  11.  
    <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4)
  12.  
    <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4, E e5)
  13.  
    <E extends Enum<E>> EnumSet<E> of(E first, E... rest)
  14.  
    // 初始集合包括参数容器中的全部元素
  15.  
    <E extends Enum<E>> EnumSet<E> copyOf(EnumSet<E> s)
  16.  
    <E extends Enum<E>> EnumSet<E> copyOf(Collection<E> c)

能够看到,EnumSet有不少重载形式的of方法,最后一个接受的的是可变参数,其余重载方法看上去是多余的,之因此有其余重载方法是由于可变参数的运行效率低一些。

应用场景

下面,咱们经过一个场景来看EnumSet的应用。

想象一个场景,在一些工做中,好比医生、客服,不是每一个工做人员天天都在的,每一个人可工做的时间是不同的,好比张三多是周一和周三,李四多是周四和周六,给定每一个人可工做的时间,咱们可能有一些问题须要回答,好比:

  • 有没有哪天一我的都不会来?
  • 有哪些天至少会有一我的来?
  • 有哪些天至少会有两我的来?
  • 有哪些天全部人都会来,以便开会?
  • 哪些人周一和周二都会来? 

使用EnumSet,能够方便高效地回答这些问题,怎么作呢?咱们先来定义一个表示工做人员的类Worker,以下所示:

  1.  
    class Worker {
  2.  
    String name;
  3.  
    Set<Day> availableDays;
  4.  
     
  5.  
    public Worker(String name, Set<Day> availableDays) {
  6.  
    this.name = name;
  7.  
    this.availableDays = availableDays;
  8.  
    }
  9.  
     
  10.  
    public String getName() {
  11.  
    return name;
  12.  
    }
  13.  
     
  14.  
    public Set<Day> getAvailableDays() {
  15.  
    return availableDays;
  16.  
    }
  17.  
    }

为演示方便,将全部工做人员的信息放到一个数组workers中,以下所示:

  1.  
    Worker[] workers = new Worker[]{
  2.  
    new Worker("张三", EnumSet.of(
  3.  
    Day.MONDAY, Day.TUESDAY, Day.WEDNESDAY, Day.FRIDAY)),
  4.  
    new Worker("李四", EnumSet.of(
  5.  
    Day.TUESDAY, Day.THURSDAY, Day.SATURDAY)),
  6.  
    new Worker("王五", EnumSet.of(
  7.  
    Day.TUESDAY, Day.THURSDAY)),
  8.  
    };

每一个工做人员的可工做时间用一个EnumSet表示。有了这个信息,咱们就能够回答以上的问题了。

哪些天一我的都不会来?代码能够为:

  1.  
    Set<Day> days = EnumSet.allOf(Day.class);
  2.  
    for(Worker w : workers){
  3.  
    days.removeAll(w.getAvailableDays());
  4.  
    }
  5.  
    System.out.println(days);

days初始化为全部值,而后遍历workers,从days中删除可工做的全部时间,最终剩下的就是一我的都不会来的时间,这实际是在求worker时间并集的补集,输出为:

[SUNDAY]

有哪些天至少会有一我的来?就是求worker时间的并集,代码能够为:

  1.  
    Set<Day> days = EnumSet.noneOf(Day.class);
  2.  
    for(Worker w : workers){
  3.  
    days.addAll(w.getAvailableDays());
  4.  
    }
  5.  
    System.out.println(days);

输出为:

[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY]

有哪些天全部人都会来?就是求worker时间的交集,代码能够为:

  1.  
    Set<Day> days = EnumSet.allOf(Day.class);
  2.  
    for(Worker w : workers){
  3.  
    days.retainAll(w.getAvailableDays());
  4.  
    }
  5.  
    System.out.println(days);

输出为:

[TUESDAY]

哪些人周一和周二都会来?使用containsAll方法,代码能够为:

  1.  
    Set<Worker> availableWorkers = new HashSet<Worker>();
  2.  
    for(Worker w : workers){
  3.  
    if(w.getAvailableDays().containsAll(
  4.  
    EnumSet.of( Day.MONDAY,Day.TUESDAY))){
  5.  
    availableWorkers.add(w);
  6.  
    }
  7.  
    }
  8.  
    for(Worker w : availableWorkers){
  9.  
    System.out.println(w.getName());
  10.  
    }

输出为:

张三

哪些天至少会有两我的来?咱们先使用EnumMap统计天天的人数,而后找出至少有两我的的天,代码能够为:

  1.  
    Map< Day, Integer> countMap = new EnumMap<>(Day.class);
  2.  
    for(Worker w : workers){
  3.  
    for(Day d : w.getAvailableDays()){
  4.  
    Integer count = countMap. get(d);
  5.  
    countMap.put(d, count== null?1:count+1);
  6.  
    }
  7.  
    }
  8.  
    Set<Day> days = EnumSet.noneOf(Day.class);
  9.  
    for(Map.Entry<Day, Integer> entry : countMap.entrySet()){
  10.  
    if(entry.getValue()>=2){
  11.  
    days.add(entry.getKey());
  12.  
    }
  13.  
    }
  14.  
    System.out.println(days);

输出为:

[TUESDAY, THURSDAY]

理解了EnumSet的使用,下面咱们来看它是怎么实现的。

实现原理

位向量

EnumSet是使用位向量实现的,什么是位向量呢?就是用一个位表示一个元素的状态,用一组位表示一个集合的状态,每一个位对应一个元素,而状态只可能有两种。

对于以前的枚举类Day,它有7个枚举值,一个Day的集合就能够用一个字节byte表示,最高位不用,设为0,最右边的位对应顺序最小的枚举值,从右到左,每位对应一个枚举值,1表示包含该元素,0表示不含该元素。

好比,表示包含Day.MONDAY,Day.TUESDAY,Day.WEDNESDAY,Day.FRIDAY的集合,位向量图示结构以下:

对应的整数是23。

位向量能表示的元素个数与向量长度有关,一个byte类型能表示8个元素,一个long类型能表示64个元素,那EnumSet用的长度是多少呢?

EnumSet是一个抽象类,它没有定义使用的向量长度,它有两个子类,RegularEnumSet和JumboEnumSet。RegularEnumSet使用一个long类型的变量做为位向量,long类型的位长度是64,而JumboEnumSet使用一个long类型的数组。若是枚举值个数小于等于64,则静态工厂方法中建立的就是RegularEnumSet,大于64的话就是JumboEnumSet。

内部组成

理解了位向量的基本概念,咱们来看EnumSet的实现,同EnumMap同样,它也有表示类型信息和全部枚举值的实例变量,以下所示:

  1.  
    final Class<E> elementType;
  2.  
    final Enum[] universe;

elementType表示类型信息,universe表示枚举类的全部枚举值。

EnumSet自身没有记录元素个数的变量,也没有位向量,它们是子类维护的。

对于RegularEnumSet,它用一个long类型表示位向量,代码为:

private long elements = 0L;

它没有定义表示元素个数的变量,是实时计算出来的,计算的代码是:

  1.  
    public int size() {
  2.  
    return Long.bitCount(elements);
  3.  
    }

对于JumboEnumSet,它用一个long数组表示,有单独的size变量,代码为:

  1.  
    private long elements[];
  2.  
    private int size = 0;

静态工厂方法

咱们来看EnumSet的静态工厂方法noneOf,代码为:

  1.  
    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
  2.  
    Enum[] universe = getUniverse(elementType);
  3.  
    if (universe == null)
  4.  
    throw new ClassCastException(elementType + " not an enum");
  5.  
     
  6.  
    if (universe.length <= 64)
  7.  
    return new RegularEnumSet<>(elementType, universe);
  8.  
    else
  9.  
    return new JumboEnumSet<>(elementType, universe);
  10.  
    }

getUniverse的代码与上节介绍的EnumMap是同样的,就不赘述了。若是元素个数不超过64,就建立RegularEnumSet,不然建立JumboEnumSet。

RegularEnumSet和JumboEnumSet的构造方法为:

  1.  
    RegularEnumSet( Class<E>elementType, Enum[] universe) {
  2.  
    super(elementType, universe);
  3.  
    }
  4.  
    JumboEnumSet( Class<E>elementType, Enum[] universe) {
  5.  
    super(elementType, universe);
  6.  
    elements = new long[(universe.length + 63) >>> 6];
  7.  
    }

它们都调用了父类EnumSet的构造方法,其代码为:

  1.  
    EnumSet(Class<E>elementType, Enum[] universe) {
  2.  
    this.elementType = elementType;
  3.  
    this.universe = universe;
  4.  
    }

就是给实例变量赋值,JumboEnumSet根据元素个数分配足够长度的long数组。

其余工厂方法基本都是先调用noneOf构造一个空的集合,而后再调用添加方法,咱们来看添加方法。

添加元素

RegularEnumSet的add方法的代码为:

  1.  
    public boolean add(E e) {
  2.  
    typeCheck(e);
  3.  
     
  4.  
    long oldElements = elements;
  5.  
    elements |= ( 1L << ((Enum)e).ordinal());
  6.  
    return elements != oldElements;
  7.  
    }

主要代码是按位或操做:

elements |= (1L << ((Enum)e).ordinal());

(1L << ((Enum)e).ordinal())将元素e对应的位设为1,与现有的位向量elements相或,就表示添加e了。从集合论的观点来看,这就是求集合的并集。

JumboEnumSet的add方法的代码为:

  1.  
    public boolean add(E e) {
  2.  
    typeCheck(e);
  3.  
     
  4.  
    int eOrdinal = e.ordinal();
  5.  
    int eWordNum = eOrdinal >>> 6;
  6.  
     
  7.  
    long oldElements = elements[eWordNum];
  8.  
    elements[eWordNum] |= ( 1L << eOrdinal);
  9.  
    boolean result = (elements[eWordNum] != oldElements);
  10.  
    if (result)
  11.  
    size++;
  12.  
    return result;
  13.  
    }

 

与RegularEnumSet的add方法的区别是,它先找对应的数组位置,eOrdinal >>> 6就是eOrdinal除以64,eWordNum就表示数组索引,有了索引以后,其余操做与RegularEnumSet就相似了。

对于其余操做,JumboEnumSet的思路是相似的,主要算法与RegularEnumSet同样,主要是增长了寻找对应long位向量的操做,或者有一些循环处理,逻辑也都比较简单,后文就只介绍RegularEnumSet的实现了。

RegularEnumSet的addAll方法的代码为:

  1.  
    public boolean addAll(Collection<? extends E> c) {
  2.  
    if (!(c instanceof RegularEnumSet))
  3.  
    return super.addAll(c);
  4.  
     
  5.  
    RegularEnumSet es = (RegularEnumSet)c;
  6.  
    if (es.elementType != elementType) {
  7.  
    if (es.isEmpty())
  8.  
    return false;
  9.  
    else
  10.  
    throw new ClassCastException(
  11.  
    es.elementType + " != " + elementType);
  12.  
    }
  13.  
     
  14.  
    long oldElements = elements;
  15.  
    elements |= es.elements;
  16.  
    return elements != oldElements;
  17.  
    }

类型正确的话,就是按位或操做。

删除元素

remove方法的代码为:

  1.  
    public boolean remove(Object e) {
  2.  
    if (e == null)
  3.  
    return false;
  4.  
    Class eClass = e.getClass();
  5.  
    if (eClass != elementType && eClass.getSuperclass() != elementType)
  6.  
    return false;
  7.  
     
  8.  
    long oldElements = elements;
  9.  
    elements &= ~( 1L << ((Enum)e).ordinal());
  10.  
    return elements != oldElements;
  11.  
    }

主要代码是:

elements &= ~(1L << ((Enum)e).ordinal());

~是取反,该代码将元素e对应的位设为了0,这样就完成了删除。

从集合论的观点来看,remove就是求集合的差,A-B等价于A∩B’,B’表示B的补集。代码中,elements至关于A,(1L << ((Enum)e).ordinal())至关于B,~(1L << ((Enum)e).ordinal())至关于B’,elements &= ~(1L << ((Enum)e).ordinal())就至关于A∩B’,即A-B。

查看是否包含某元素

contains方法的代码为:

  1.  
    public boolean contains(Object e) {
  2.  
    if (e == null)
  3.  
    return false;
  4.  
    Class eClass = e.getClass();
  5.  
    if (eClass != elementType && eClass.getSuperclass() != elementType)
  6.  
    return false;
  7.  
     
  8.  
    return (elements & (1L << ((Enum)e).ordinal())) != 0;
  9.  
    }

代码也很简单,按位与操做,不为0,则表示包含。

查看是否包含集合中的全部元素

containsAll方法的代码为:

  1.  
    public boolean containsAll(Collection<?> c) {
  2.  
    if (!(c instanceof RegularEnumSet))
  3.  
    return super.containsAll(c);
  4.  
     
  5.  
    RegularEnumSet es = (RegularEnumSet)c;
  6.  
    if (es.elementType != elementType)
  7.  
    return es.isEmpty();
  8.  
     
  9.  
    return (es.elements & ~elements) == 0;
  10.  
    }

最后的位操做有点晦涩。咱们从集合论的角度解释下,containsAll就是在检查参数c表示的集合是否是当前集合的子集。通常而言,集合B是集合A的子集,即B⊆A,等价于A’∩B是空集∅,A’表示A的补集,以下图所示:

 

上面代码中,elements至关于A,es.elements至关于B,~elements至关于求A的补集,(es.elements & ~elements) == 0;就是在验证A’∩B是否是空集,即B是否是A的子集。

只保留参数集合中有的元素

retainAll方法的代码为:

  1.  
    public boolean retainAll(Collection<?> c) {
  2.  
    if (!(c instanceof RegularEnumSet))
  3.  
    return super.retainAll(c);
  4.  
     
  5.  
    RegularEnumSet<?> es = (RegularEnumSet<?>)c;
  6.  
    if (es.elementType != elementType) {
  7.  
    boolean changed = (elements != 0);
  8.  
    elements = 0;
  9.  
    return changed;
  10.  
    }
  11.  
     
  12.  
    long oldElements = elements;
  13.  
    elements &= es.elements;
  14.  
    return elements != oldElements;
  15.  
    }

从集合论的观点来看,这就是求集合的交集,因此主要代码就是按位与操做,容易理解。

求补集

EnumSet的静态工厂方法complementOf是求补集,它调用的代码是:

  1.  
    void complement() {
  2.  
    if (universe.length != 0) {
  3.  
    elements = ~elements;
  4.  
    elements &= - 1L >>> -universe.length; // Mask unused bits
  5.  
    }
  6.  
    }

这段代码也有点晦涩,elements=~elements比较容易理解,就是按位取反,至关于就是取补集,但咱们知道elements是64位的,当前枚举类可能没有用那么多位,取反后高位部分都变为了1,须要将超出universe.length的部分设为0。下面代码就是在作这件事:

elements &= -1L >>> -universe.length; 

-1L是64位全1的二进制,咱们在剖析Integer一节介绍过移动位数是负数的状况,上面代码至关于:

elements &= -1L >>> (64-universe.length); 

若是universe.length为7,则-1L>>>(64-7)就是二进制的1111111,与elements相与,就会将超出universe.length部分的右边的57位都变为0。

实现原理小结

以上就是EnumSet的基本实现原理,内部使用位向量,表示很简洁,节省空间,大部分操做都是按位运算,效率极高。

小结

本节介绍了EnumSet的用法和实现原理,用法上,它是处理枚举类型数据的一把利器,简洁方便,实现原理上,它使用位向量,精简高效。

对于只有两种状态,且须要进行集合运算的数据,使用位向量进行表示、位运算进行处理,是计算机程序中一种经常使用的思惟方式。

至此,关于具体的容器类,咱们就介绍完了。Java容器类中还有一些过期的容器类,以及一些不经常使用的类,咱们就不介绍了。

在介绍具体容器类的过程当中,咱们忽略了一个实现细节,那就是,全部容器类其实都不是从头构建的,它们都继承了一些抽象容器类。这些抽象类提供了容器接口的部分实现,方便了Java具体容器类的实现。若是咱们须要实现自定义的容器类,也应该考虑从这些抽象类继承。

那,具体都有什么抽象类?它们都提供了哪些基础功能?如何进行扩展呢?让咱们下节来探讨。