`

深入学习EnumSet

阅读更多
Set接口的实现类HashSet/TreeSet,它们内部都是用对应的HashMap/TreeMap实现的,
但EnumSet的实现与EnumMap没有任何关系,而是用极为精简和高效的位向量实现的,

除了实现机制,EnumSet的用法也有一些不同。与TreeSet/HashSet不同,
EnumSet是一个抽象类,不能直接通过new新建,EnumSet提供了若干静态工厂方法创建EnumSet类型的对象,比如:
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType)
noneOf方法会创建一个指定枚举类型的EnumSet,不含任何元素。
创建的EnumSet对象的实际类型是EnumSet的子类

enum Color{
 
    RED,BLUE,YELLOW
}

Set<Color> colorSet = EnumSet.noneOf(Color.class);

colorSet.add(Color.RED);

colorSet.add(Color.BLUE);

System.out.println(colorSet);


输出结果为
[RED, BLUE]

EnumSet还有很多其他静态工厂方法,如下所示(省略了修饰public static):
// 初始集合包括枚举值中指定范围的元素
<E extends Enum<E>> EnumSet<E> range(E from, E to)
// 初始集合包括指定集合的补集
<E extends Enum<E>> EnumSet<E> complementOf(EnumSet<E> s)


可以看到,EnumSet有很多重载形式的of方法,最后一个接受的的是可变参数,其他重载方法看上去是多余的,之所以有其他重载方法是因为可变参数的运行效率低一些。


EnumSet的应用:
下面,我们通过一个场景来看EnumSet的应用。
想象一个场景,在一些工作中,比如医生、客服,不是每个工作人员每天都在的,每个人可工作的时间是不一样的,比如张三可能是周一和周三,李四可能是周四和周六,给定每个人可工作的时间,我们可能有一些问题需要回答,比如:
有没有哪天一个人都不会来?
有哪些天至少会有一个人来?
有哪些天至少会有两个人来?
有哪些天所有人都会来,以便开会?
哪些人周一和周二都会来? 
使用EnumSet,可以方便高效地回答这些问题,怎么做呢?我们先来定义一个表示工作人员的类Worker,如下所示:

enum Day {
    MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY, SUNDAY
}


class Worker {
    String name;
    Set<Day> availableDays;
    
    public Worker(String name, Set<Day> availableDays) {
        this.name = name;
        this.availableDays = availableDays;
    }
    
    public String getName() {
        return name;
    }
    
    public Set<Day> getAvailableDays() {
        return availableDays;
    }
}




每个工作人员的可工作时间用一个EnumSet表示。有了这个信息,我们就可以回答以上的问题了。
哪些天一个人都不会来?代码可以为:
Set<Day> days = EnumSet.allOf(Day.class);

for(Worker w : workers){
 
    days.removeAll(w.getAvailableDays());
}
}

System.out.println(days);

days初始化为所有值,然后遍历workers,从days中删除可工作的所有时间,最终剩下的就是一个人都不会来的时间,这实际是在求worker时间并集的补集,输出为:
[SUNDAY]

Set<Day> days = EnumSet.noneOf(Day.class);

for(Worker w : workers){
 
    days.addAll(w.getAvailableDays());
}
}
S
System.out.println(days);
输出为:
[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY



有哪些天所有人都会来?就是求worker时间的交集,代码可以为:
Set<Day> days = EnumSet.allOf(Day.class);

for(Worker w : workers){
 
    days.retainAll(w.getAvailableDays());
}
}
S
System.out.println(days);
输出为:

[TUESDAY]


.....

实现原理
位向量
EnumSet是使用位向量实现的,什么是位向量呢?就是用一个位表示一个元素的状态,用一组位表示一个集合的状态,每个位对应一个元素,而状态只可能有两种。
对于之前的枚举类Day,它有7个枚举值,一个Day的集合就可以用一个字节byte表示,最高位不用,设为0,最右边的位对应顺序最小的枚举值,从右到左,每位对应一个枚举值,1表示包含该元素,0表示不含该元素。
比如,表示包含Day.MONDAY,Day.TUESDAY,Day.WEDNESDAY,Day.FRIDAY的集合,位向量图示结构如下

0         0           0          1             0           1            1         1
        周日     周六     周五    周四     周三     周二   周一

对应的整数是23。
位向量能表示的元素个数与向量长度有关,一个byte类型能表示8个元素,一个long类型能表示64个元素,那EnumSet用的长度是多少呢?
EnumSet是一个抽象类,它没有定义使用的向量长度,它有两个子类,RegularEnumSet和JumboEnumSet。RegularEnumSet使用一个long类型的变量作为位向量,long类型的位长度是64,而JumboEnumSet使用一个long类型的数组。如果枚举值个数小于等于64,则静态工厂方法中创建的就是RegularEnumSet,大于64的话就是JumboEnumSet。



我们来看EnumSet的实现,它有表示类型信息和所有枚举值的实例变量,如下所示:
final Class<E> elementType;

final Enum[] universe;

elementType表示类型信息,universe表示枚举类的所有枚举值。
EnumSet自身没有记录元素个数的变量,也没有位向量,它们是子类维护的。
对于RegularEnumSet,它用一个long类型表示位向量,代码为:

private long elements = 0L;

它没有定义表示元素个数的变量,是实时计算出来的,计算的代码是:
public int size() {
 
    return Long.bitCount(elements);
}
}

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

private int size = 0;




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

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);
}

RegularEnumSet和JumboEnumSet的构造方法为:

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



它们都调用了父类EnumSet的构造方法,其代码为:
EnumSet(Class<E>elementType, Enum[] universe) {
 
    this.elementType = elementType;
 
    this.universe    = universe;
}
}



添加元素:
RegularEnumSet的add方法的代码为:

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


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



与RegularEnumSet的add方法的区别是,它先找对应的数组位置,eOrdinal >>> 6就是eOrdinal除以64,eWordNum就表示数组索引,有了索引之后,其他操作与RegularEnumSet就类似了。
对于其他操作,JumboEnumSet的思路是类似的,主要算法与RegularEnumSet一样,主要是增加了寻找对应long位向量的操作,或者有一些循环处理,逻辑也都比较简单,后文就只介绍RegularEnumSet的实现了。
RegularEnumSet的addAll方法的代码为:

public boolean addAll(Collection<? extends E> c) {
    if (!(c instanceof RegularEnumSet))
        return super.addAll(c);

    RegularEnumSet es = (RegularEnumSet)c;
    if (es.elementType != elementType) {
        if (es.isEmpty())
            return false;
        else
            throw new ClassCastException(
                es.elementType + " != " + elementType);
    }

    long oldElements = elements;
    elements |= es.elements;
    return elements != oldElements;
}




删除元素
remove方法的代码为:

public boolean remove(Object e) {
    if (e == null)
        return false;
    Class eClass = e.getClass();
    if (eClass != elementType && eClass.getSuperclass() != elementType)
        return false;

    long oldElements = elements;
    elements &= ~(1L << ((Enum)e).ordinal());
    return elements != oldElements;
}




~是取反,该代码将元素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。



查看是否包含某元素:
public boolean contains(Object e) {
    if (e == null)
        return false;
    Class eClass = e.getClass();
    if (eClass != elementType && eClass.getSuperclass() != elementType)
        return false;

    return (elements & (1L << ((Enum)e).ordinal())) != 0; //这里判断交集是否为0 即可。
}




查看是否包含集合中的所有元素
containsAll方法的代码为:

public boolean containsAll(Collection<?> c) {
    if (!(c instanceof RegularEnumSet))
        return super.containsAll(c);

    RegularEnumSet es = (RegularEnumSet)c;
    if (es.elementType != elementType)
        return es.isEmpty();

    return (es.elements & ~elements) == 0; /
}



containsAll就是在检查参数c表示的集合是不是当前集合的子集。一般而言,集合B是集合A的子集,即B⊆A,等价于A'∩B是空集∅,A'表示A的补集



retainAll方法的代码为:
public boolean retainAll(Collection<?> c) {
    if (!(c instanceof RegularEnumSet))
        return super.retainAll(c);

    RegularEnumSet<?> es = (RegularEnumSet<?>)c;
    if (es.elementType != elementType) {
        boolean changed = (elements != 0);
        elements = 0;
        return changed;
    }

    long oldElements = elements;
    elements &= es.elements;
    return elements != oldElements;
}



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

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


-1L是64位全1的二进制(补码表示)
上面代码相当于:

elements &= -1L >>> (64-universe.length);
如果universe.length为7,则-1L>>>(64-7)就是二进制的1111111,与elements相与,就会将超出universe.length部分的右边的57位都变为0。
实现原理小结
以上就是EnumSet的基本实现原理,内部使用位向量,表示很简洁,节省空间,大部分操作都是按位运算,效率极高。

分享到:
评论

相关推荐

    java集合-EnumSet的使用

    EnumSet 是 Java 中用于存储枚举类型元素的集合类。它是 AbstractSet 的子类,并专门为枚举类型设计,提供了高效的实现。 下面是关于 EnumSet 的一些重要信息: 存储枚举元素:EnumSet 只能存储同一个枚举类型的...

    阅读EnumSet抽象类

    主要介绍了阅读EnumSet抽象类源码,具有一定参考价值,需要的朋友可以了解下。

    Java中EnumSet代替位域代码详解

    主要介绍了Java中EnumSet代替位域代码详解,分享了相关代码示例,小编觉得还是挺不错的,具有一定借鉴价值,需要的朋友可以参考下

    Java EnumSet工作原理初窥

    EnumSet是Java枚举类型的泛型容器,Java既然有了SortedSet、TreeSet、HashSet等容器,为何还要多一个EnumSet呢?答案肯定是EnumSet有一定的特性,举个例子,EnumSet的速度很快。其他特性不一一列举了,毕竟本文的...

    说说Java中的枚举 转 可以了,够了 包括EnumSet(Map)

    NULL 博文链接:https://mlaaalm.iteye.com/blog/694720

    一文弄懂EnumMap和EnumSet

    一文弄懂EnumMap和EnumSet 简介 一般来说我们会选择使用HashMap来存储key-value格式的数据,考虑这样的特殊情况,一个HashMap的key都来自于一个Enum类,这样的情况则可以考虑使用本文要讲的EnumMap。 EnumMap 先看...

    enumset-用于创建紧凑的枚举集的库-Rust开发

    enumset一个用于定义可在紧凑位集中使用的枚举的库。 它支持最多128个变量的枚举,并具有在枚举中使用这些集合的宏。用于定义可在紧凑位集中使用的枚举的库。 它支持最多128个变量的枚举,并具有一个宏以在常量中...

    Options:有时在某些情况下,您想在OptionSet中使用Enum或希望Enum由Raw的Int类型支持,但同时也具有String标签

    目录介绍特征安装用法设置一个MappedValueRepresentable枚举使用MappedValueCollectionRepresented 使用MappedEnum类型的可编码枚举在ESet中使用OptionSet中的EnumSet 将EnumSet转换为Enum数组使用...

    Java—Set集合详解(HashSet/LinkedHashSet/TreeSet/EnumSet)

    Set集合介绍 Set集合的概念   Set集合类似于一个容器,程序把很多对象保存到Set集合中,Set集合对添加顺序不记录,当有重复的对象保存到Set集合时,不会新增后加的重复对象。 Set集合的特点 Set集合无重复元素,...

    Java实现高效的枚举元素集合

    Set是Java集合类的重要...使用EnumSet类的add()方法添加元素,使用EnumSet类的remove()方法删除元素,使用EnumSet类的complementOf()方法获取对象的全部,使用EnumSet类的range()方法获取指定范围的元素。  代码如下

    android_external_square_javawriter

    Java编写器JavaWriter是一个实用程序类,它有助... of( PUBLIC , FINAL )) .emitField( " String " , " firstName " , EnumSet . of( PRIVATE )) .emitField( " String " , " lastName " , EnumSet . of( PRIVATE )) .

    libjavawriter

    Java编写器 JavaWriter是一个实用程序类,它有助于生成 Java 源文件。... .emitField( " String " , " firstName " , EnumSet . of( PRIVATE )) .emitField( " String " , " lastName " , EnumSet . of( PRIVATE )) .

    高级编程-java实验报告.docx

    实验目的及要求 1) 掌握Java集合框架的概念以及几种具体实现:ArrayList, LinkedList, HashSet, TreeSet, PriorityQueue; 2) 掌握Java集合框架的映射的概念以及映射的...3)枚举类型的使用,EnumSet和EnumMap的使用;

    集合效率不完全皮恩车

    以循环1000000万次为标准! 定义如下数组 public static char[] chars = {'A','B','C'...ConcurrentSkipListSet , CopyOnWriteArraySet , EnumSet…, HashSet JobStateReasons , LinkedHashSet , TreeSet Map接口 …….

    用枚举值管理项目字典的实战应用(适配器模式)(代码示例)

    用枚举值管理项目字典的实战应用(适配器模式)(代码示例) 枚举值相比常量的优势 应用场景 模拟代码实现 ...常量需要用到反射,而枚举的EnumSet提供了直接遍历的方法。 1.4 离散值面向对象,方便程序调用。

    jonykchen#effective-java-3rd-chinese#01. 考虑使用静态工厂方法替代构造方法1

    声明的返回类型的任何子类都是允许的。返回对象的类也可以随每次发布而不同。EnumSet 类(详见第 36 条)没有公共构造方法,只有静态工厂。在 OpenJDK

    yuchuangu85#Develop-Source#01. 考虑使用静态工厂方法替代构造方法1

    声明的返回类型的任何子类都是允许的。返回对象的类也可以随每次发布而不同。EnumSet 类(详见第 36 条)没有公共构造方法,只有静态工厂。在 OpenJDK

    Java期末复习——枚举与反射机制

    Java——枚举: enum关键字、Enum类 类集对枚举的支持——EnumMap类与EnumSet类 枚举类实现接口、在枚举类中定义抽象方法 Java反射机制: Class类、Class类的使用 反射的应用:取得类的结构

Global site tag (gtag.js) - Google Analytics