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

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

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

定义及说明

定义如下:

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

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

2、实现了Cloneable,即支持clone

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

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

//创建具有指定元素类型的空枚举集
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> 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(Class<E>elementType, Enum<?>[] universe){
        super(elementType, universe);
    }

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

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

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

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

EnumSet(Class<E>elementType, 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是使用位向量实现的,即使用一个位表示一个元素的状态

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,636评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,890评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,680评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,766评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,665评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,045评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,515评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,182评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,334评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,274评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,319评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,002评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,599评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,675评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,917评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,309评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,885评论 2 341

推荐阅读更多精彩内容

  • Java集合类可用于存储数量不等的对象,并可以实现常用的数据结构如栈,队列等,Java集合还可以用于保存具有映射关...
    小徐andorid阅读 1,900评论 0 13
  • Chapter 6 Enums and Annotations 枚举和注解 JAVA supports two s...
    LaMole阅读 784评论 0 2
  • Java集合框架 Java中封装了许多常用的数据结构,称为集合框架,可以有效组织数据,提高程序性能。最初Java只...
    Steven1997阅读 898评论 0 2
  • 四、集合框架 1:String类:字符串(重点) (1)多个字符组成的一个序列,叫字符串。生活中很多数据的描述都采...
    佘大将军阅读 730评论 0 2
  • 一、基础知识:1、JVM、JRE和JDK的区别:JVM(Java Virtual Machine):java虚拟机...
    杀小贼阅读 2,360评论 0 4