Java集合(十一)--EnumSet简析

EnumSet是用于枚举类型的专用Set实现。EnumSet中的所有元素必须来自单个枚举类型,该类型在创建集时显式或隐式指定。枚举集在内部表示为位向量,这种表现非常紧凑和高效。它不允许有空值,如果是试图插入空值,将会抛出NullPointerException异常,但是可以检测是否含有空值。通之前讲的其他集合一样,他也是非同步的。

EnumSet的迭代器方法返回的迭代器以其自然顺序(枚举类中枚举常量的顺序)遍历元素。返回的迭代器是弱一致的:它永远不会抛出ConcurrentModificationException,它可能会也可能不会显示迭代进行过程中对集合所做的任何修改的影响。

定义及说明

定义如下:

public abstract class EnumSet> extends AbstractSet
    implements Cloneable, java.io.Serializable{}

1、继承于AbstractSet,拥有Set基本的方法和属性

2、实现了Cloneable,即支持clone

3、实现了java.io.Serializable,即支持序列化和反序列化

不同于HashSet和TreeSet,它不是基于Map实现的。而且,它是一个抽象类。所以,我们不能通过new来实例化一个它的对象。那我们应该怎么去获取它的对象,或者说怎么去使用它呢?EnumSet提供了以下方法:

//创建具有指定元素类型的空枚举集
public static > EnumSet noneOf(Class elementType) {
        //返回包含elementType的泛型类中所有元素的数组
        Enum[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");
        //判断该泛型类中的泛型常量的数量是否大于64
        if (universe.length <= 64)
            //小于64使用RegularEnumSet的实例化对象
            return new RegularEnumSet<>(elementType, universe);
        else
            //大于64用JumboEnumSet的实例化对象
            return new JumboEnumSet<>(elementType, universe);
    }       

还有其他的方法,我们就不一一列举了。但是其中都是调用了noneOf()方法或者clone()方法。

我们发现,以枚举类中是否包含64个枚举常量为分界线,EnumSet分别会实例化RegularEnumSet(小于等于64)和JumboEnumSet(大于64)两种EnumSet的实现类的对象。那么现在,我们就先看看这两个类的构造方法:

//RegularEnumSet
RegularEnumSet(ClasselementType, Enum[] universe){
        super(elementType, universe);
    }

//JumboEnumSet
JumboEnumSet(ClasselementType, Enum[] universe) {
        super(elementType, universe);
        elements = new long[(universe.length + 63) >>> 6];
    }

它们都调用了EnumSet的以下方法做初始化:

//该集合中所有元素的枚举类
final Class elementType;

//包含elementType的所有枚举常量的数组
final Enum[] universe;

EnumSet(ClasselementType, Enum[] universe) {
        this.elementType = elementType;
        this.universe    = universe;
    }

就是给实例变量赋初始值,并且在JumboEnumSet中,初始化一个足够长度的数组。

接下来,我们分别看一下它们的add()方法。

RegularEnumSet的add()方法:

private long elements = 0L;

public boolean add(E e) {
        //检查类型是否为此枚举集的正确类型,不是则抛出异常
        typeCheck(e);
        //获取长整型elements的值,并赋予oldElements
        long oldElements = elements;
        //获取e在枚举中的位置,并将elements的二进制的对应位置的数字变为1
        elements |= (1L << ((Enum)e).ordinal());
        //判断是否添加过
        return elements != oldElements;
    }

可见RegularEnumSet的add()操作就是获取e在枚举类中所对应的位置,然后将长整型数据elements的二进制表现形式的对应位置设为1。

我们再看一下JumboEnumSet的add()方法:

private long elements[];

public boolean add(E e) {
        //检查类型是否为此枚举集的正确类型,不是则抛出异常
        typeCheck(e);
         //获取e在枚举中的位置
        int eOrdinal = e.ordinal();
        //将eOrdinal的二进制表现形式右移6位
        int eWordNum = eOrdinal >>> 6;
        
         //获取长整型elements的值,并赋予oldElements
        long oldElements = elements[eWordNum];
        //获取e在枚举中的位置,并根据某种方式给elements数组的指定索引中的整型赋值
        elements[eWordNum] |= (1L << eOrdinal);
        //判断是否添加过
        boolean result = (elements[eWordNum] != oldElements);
        if (result)
            size++;
        return result;
    }

此方法中要注意的是,它是将e在泛型类中的位置的二进制表现形式先右移了6位,将右移后的值作为数组的索引。所以,在索引为0的位置上,它可以表示的数值应该是从0B0000000到0B111111(0B表示二进制数),即从0~127。由此可推出索引为n的位置,数据的最大值为2的n+1+6次幂减1,最小值为2的n+6次幂(除了n=0的时候,最小值为0)。即通过数组的不同的索引,不同索引中不同的数字的大小,来表示枚举常量在枚举类中的位置。

其他的操作中JumboEnumSet和RegularEnumSet的思路是相同的,所以,我们就从RegularEnumSet入手分析了。

我们看一下RegularEnumSet的remove()方法:

public boolean remove(Object e) {
        //判断e是否为空及是否为指定的枚举常量值
        if (e == null)
            return false;
        Class eClass = e.getClass();
        if (eClass != elementType && eClass.getSuperclass() != elementType)
            return false;
        //将当前的elements赋值给oldElements
        long oldElements = elements;
        //先取反,再与elements做与操作
        elements &= ~(1L << ((Enum)e).ordinal());
        return elements != oldElements;
    }

其中的主要代码是"elements &= ~(1L << ((Enum)e).ordinal())"。通过取反,将其他位值为1,当前位值为0,然后与elements做与运算,以达到删除的目的。

再分析一下contains()方法:

public boolean contains(Object e) {
        //判断e是否为空及是否为指定的枚举常量值
        if (e == null)
            return false;
        Class eClass = e.getClass();
        if (eClass != elementType && eClass.getSuperclass() != elementType)
            return false;
        //按位与计算,不为0则包含
        return (elements & (1L << ((Enum)e).ordinal())) != 0;
    }

这里做了按位的与计算,即两个数值的对应位,如果都是1,则结果为才为1。这样就能判断指定的位置是否有数据。

最后,分析一下containsAll()方法:

public boolean containsAll(Collection c) {
        //判断c是否为RegularEnumSet类
        if (!(c instanceof RegularEnumSet))
            return super.containsAll(c);
        //将c的值赋予es
        RegularEnumSet es = (RegularEnumSet)c;
        
         //判断es是不是指定的枚举类型
        if (es.elementType != elementType)
            return es.isEmpty();
        
        return (es.elements & ~elements) == 0;
    }

我们看一下"(es.elements & ~elements) == 0",先将elements按位取反(我们假设将结果赋予n),然后再与es.elements做与计算。这样,如果计算结果不为0,即es.elements与n有相同的元素。也就是说es中,有elements不包含的元素。那么elements也就不包含es了。

小结

1、EnumSet中的所有元素必须来自单个枚举类型

2、EnumSet不允许有空值,但是可以检测是否含有空值

3、EnumSet的迭代器方法返回的迭代器以枚举类中枚举常量的顺序遍历元素,且永远不会抛出ConcurrentModificationException

4、EnumSet是非同步的

5、EnumSet是一个抽象类,我们对他的操作实际是对它的两个实现类JumboEnumSet或者RegularEnumSet其中之一做的操作。具体判断为,如果枚举类中的枚举常量大于64个,则使用JumboEnumSet,反之使用RegularEnumSet

6、EnumSet是使用位向量实现的,即使用一个位表示一个元素的状态

你可能感兴趣的:(Java集合(十一)--EnumSet简析)