Java 之EnumSet与EnumMap

        Java 之EnumSet与EnumMap

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

  首先以事实说话,存在这样一个EnumSet,它有50个枚举值T0~T49,将50个值插入到容器(HashSet、EnumSet)中,为一个操作,将50个枚举值移出做为第二个操作。把第一个和第二个操作执行的总时间设定为一个周期,拿HashSet操作的一个周期和EnumSet的一个周期做比较自然没什么意义,所以我们用50个周期的和做为比较,HashSet耗费9ms,EnumSet耗费4ms(这个结果只说明同样的操作EnumSet比HashSet更快,不做为其他参考依据,因为这个时间不是线程独占时间)。以下是代码和结果:

 

public class EnumSetTest{

   

    private static EnumTest[] enumTestArr = EnumTest.values();

 

    public static void main(String[] args) {

        Set set = newHashSet();

        int i =0;

        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

       System.out.println("HashSet...Begin " + df.format(new Date()));

        while (i<= 1000) {

            addEnumerate(set);

            removeEnumerate(set);

            i++;

        }

       System.out.println("HashSet...End " + df.format(new Date()));

 

        EnumSet enumSet =EnumSet.noneOf(EnumTest.class);

        i = 0;

       System.out.println("EnumSet...Begin " + df.format(new Date()));

        while (i<= 1000) {

            addEnumerate(enumSet);

            removeEnumerate(enumSet);

            i++;

        }

        System.out.println("EnumSet...End" + df.format(new Date()));

    }

    /*

    * HashSet...Begin 2015-01-03 21:11:51.579

    * HashSet...End  2015-01-03 21:11:51.588

    * EnumSet...Begin 2015-01-03 21:11:51.589

    * EnumSet...End 2015-01-03 21:11:51.593

    * */

 

    private static void addEnumerate(Set set) {

        for(EnumTest t : enumTestArr) {

            set.add(t);

        }

    }

 

    private static void removeEnumerate(Set set) {

        for(EnumTest t : enumTestArr) {

            set.remove(t);

        }

    }

}

那为什么EnumSet比较快呢,EnumSet是一个抽象方法,本次测试是用到的EnumSet的实现RegularEnumSet,RegularEnumSet add方法的源码如下:

    public boolean add(E e) {

        typeCheck(e);

 

        longoldElements = elements;

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

        returnelements != oldElements;

   

RegularEnumSet的源码

从Add方法的源码可以看出add方法实际只是对长整型数据element做了一个操作,也就是说EnumSet实际上将枚举值保存在一个长整型数据上。没个枚举值占用一bit。每次添加做的主要操作是1、类型检查 2、 添加枚举值 3、判断枚举值是否已经添加过了。 

现在模拟一个场景来说明EnumSet工作原理。新建一个EnumSet(Set1),并向Set1中添加EnumTest.T3(ordinal:3),代码如下

        EnumSet set1 =EnumSet.noneOf(EnumTest.class);

        set1.add(EnumTest.T3);

element本来有64个bit,这里就简略画了。这里有一个问题,就是枚举值的个数超过64个怎么办?超过64个了就用EnumSet的另一个实现JumboEnumSet

EnumSet 是一个与枚举类型一起使用的专用 Set 实现。枚举set中所有元素都必须来自单个枚举类型(即必须是同类型,且该类型是Enum的子类)。
 枚举类型在创建 set 时显式或隐式地指定。枚举 set 在内部表示为位向量。 此表示形式非常紧凑且高效。此类的空间和时间性能应该很好,
 
足以用作传统上基于 int 位标志的替换形式,具有高品质、类型安全的优势。
 
如果指定的 collection 也是一个枚举 set,则批量操作(如 containsAll  retainAll)也应运行得非常快。
 
 iterator 方法返回的迭代器按其自然顺序 遍历这些元素(该顺序是声明枚举常量的顺序)。
 
返回的迭代器是弱一致的:它从不抛出 ConcurrentModificationException,也不一定显示在迭代进行时发生的任何 set 修改的效果。
 
不允许使用 null 元素。试图插入 null 元素将抛出 NullPointerException。但是,
 
试图测试是否出现 null 元素或移除 null 元素将不会抛出异常。
 
像大多数 collection 一样,EnumSet 是不同步的。如果多个线程同时访问一个枚举 set
 
并且至少有一个线程修改该 set,则此枚举 set 在外部应该是同步的。这通常是通过对自然封装该枚举 set 的对象执行同步操作来完成的。
 
如果不存在这样的对象,则应该使用 Collections.synchronizedSet(java.util.Set) 方法来包装 set
 
最好在创建时完成这一操作,以防止意外的非同步访问:
  Set s = Collections.synchronizedSet(EnumSet.noneOf(Foo.class));
 
实现注意事项:所有基本操作都在固定时间内执行。虽然并不保证,但它们很可能比其 HashSet 副本更快。
 
如果参数是另一个 EnumSet 实例,则诸如 addAll()  AbstractSet.removeAll(java.util.Collection) 之类的批量操作
 
也会在固定时间内执行。
注意1:不允许使用 null 元素。试图插入 null 元素将抛出 NullPointerException。但是,
 
试图测试是否出现 null 元素或移除 null 元素将不会抛出异常。
注意2EnumSet是不同步的。不是线程安全的。
注意3EnumSet的本质就为枚举类型定制的一个Set,且枚举set中所有元素都必须来自单个枚举类型。
注意4:关于EnumSet的存储,文档中提到是这样的。 “枚举 set 在内部表示为位向量。
 
我想它应该是用一个bit为来表示的于之对应的枚举变量是否在集合中。
 
比如:0x1001
 
假如约定从低位开始,就表示第0个,和第三个枚举类型变量在EnumSet中。
 
这样的话空间和时间性能也就应该很好。
注意5:至于Enum的枚举常量的位置(序数)可以用Enumordinal()方法得到。
注意6:jdk内部可以一个数组的形式一个枚举的枚举常量。
下面是来自来JDKRegularEnumSet.java的一个示例
    private static > E[] getUniverse(Class elementType) {
        return SharedSecrets.getJavaLangAccess()
     .getEnumConstantsShared(elementType);
    }

注意7:元素属于哪种枚举类型必须在创建 set 时显式或隐式地指定.
注意8:关于枚举类型的更多知识可参考《枚举类型》

Enumset是个虚类,我们只能通过它提供的静态方法来返回Enumset的实现类的实例。

返回EnumSet的两种不同的实现:如果EnumSet大小小于64

就返回RegularEnumSet实例(当然它继承自EnumSet),这个EnumSet实际上至用了一个long来存储这个EnumSet

如果 EnumSet大小大于等于64,则返回JumboEnumSet实例,它使用一个long[]来存储。这样做的好处很明显: 大多数情况下返回的RegularEnumSet效率比JumboEnumSet高很多。

附录:Java的类库实在是很多,以至于很多人都不太了解,结果总是自己造轮子。

下面汇总了Java中的一些数据结构,加上一些实现的分析,同时备忘。

至于时间复杂度,个人觉得写出来的用处不大。如果明白它是怎么实现的,那自然就知道它的时间复杂度。

如果不理解它的实现,把时间复杂度背得再熟也没用。

 

接口:

Collection

子接口:

BlockingDeque, BlockingQueue, Deque, List, NavigableSet, Queue, Set, SortedSet 

实现类:

ArrayBlockingQueue, ArrayDeque, ArrayList,  ConcurrentLinkedQueue, ConcurrentSkipListSet, CopyOnWriteArrayList, CopyOnWriteArraySet, DelayQueue, EnumSet, HashSet, LinkedBlockingDeque, LinkedBlockingQueue, LinkedHashSet, LinkedList, PriorityBlockingQueue, PriorityQueue, Stack, SynchronousQueue, TreeSet, Vector 

 

List

实现类:

ArrayList, CopyOnWriteArrayList, LinkedList,Stack, Vector 

 

Queue

子接口:

BlockingDeque, BlockingQueue, Deque 

实现类:

ArrayBlockingQueue, ArrayDeque, ConcurrentLinkedQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, LinkedList, PriorityBlockingQueue, PriorityQueue, SynchronousQueue 

 

Set

子接口:

NavigableSet, SortedSet 

实现类:

ConcurrentSkipListSet, CopyOnWriteArraySet, EnumSet, HashSet, LinkedHashSet, TreeSet 

 

Map

子接口:

ConcurrentMap, ConcurrentNavigableMap,  SortedMap 

实现类:

ConcurrentHashMap, ConcurrentSkipListMap, EnumMap, HashMap, Hashtable, IdentityHashMap, LinkedHashMap, TreeMap, WeakHashMap 

 

 

并发与线程安全等

通常含有Concurrent,CopyOnWrite,Blocking的是线程安全的,但是这些线程安全通常是有条件的,所以在使用前一定要仔细阅读文档。

 

 

具体实现:

List系列:

ArrayList,如其名,但是其容量增长计划是newCapacity = (oldCapacity * 3)/2 + 1,和C++通常的Vector是翻倍的策略不同。

CopyOnWriteArrayList,里面有一个ReentrantLock,每当add时,都锁住,把所有的元素都复制到一个新的数组上。

只保证历遍操作是线程安全的,get操作并不保证,也就是说如果先得到size,再调用get(size-1),有可能会失效

那么CopyOnWriteArrayList是如何实现线程安全的迭代操作?

在迭代器中保存原数组。

LinkedList,标准双向链表

Vector,过时,多数方法上加上了synchronized

Stack,继承自Vector,过时,优先应使用 Deque stack = new ArrayDeque();

 

Queue系列:

LinkedList,见List系列

ArrayDeque,内部用一个数组保存元素,有int类型head和tail的。

PriorityQueue,内部用一个数组来保存元素,但数组是以堆的形式来组织的,因此是有序的。

PriorityBlockingQueue,包装了一个PriorityQueue,一个ReentrantLock,一个Condition,TODO

ArrayBlockingQueue,TODO

ConcurrentLinkedQueue,TODO

DelayQueue,TODO

LinkedBlockingDeque,TODO

LinkedBlockingQueue,TODO

SynchronousQueue,TODO

 

Deque(双端队列)系列:

ArrayDeque,见Queue系列

LinkedList,见List系列

LinkedBlockingDeque,TODO

 

 

Set系列:

HashSet,包装了一个HashMap:

    public HashSet() {

        map = new HashMap();

    }

TreeSet,包装了一个TreeMap,参考HashSet

LinkedHashSet,包装了LinkedHashMap,参考HashSet

EnumSet,TODO

CopyOnWriteArraySet,简单包装了CopyOnWriteArrayList,注意这个Set的get的时间复杂度。

ConcurrentSkipListSet,包装了一个ConcurrentSkipListMap,参考HashSet。

 

 

Map系列:

HashMap,标准链地址法实现

TreeMap,红黑二叉树

LinkedHashMap,在Entry中增加before和after指针把HashMap中的元素串联起来,这样在迭代时,就可以按插入顺序历遍。

EnumMap,TODO

ConcurrentHashMap,参考之前的文章

ConcurrentSkipListMap,TODO,log(n)的时间复杂度,有点像多级链表保存的,貌似有点像redis中的SortedSet的实现

Hashtable,过时

IdentityHashMap,正常的HashMap中比较是用equals方法,这个用的是“==”比较符

WeakHashMap,弱引用的HashMap,正常的HashMap是强引用,即里面的value不会被GC回收,在WeakHashMap中,V中最好是WeakReference类型的,用像这样的代码:m.put(key, new WeakReference(value))。

 

 

其它的一些实用的第三方的数据结构:

LRUCache,LongHashMap,Java7中的LinkedTransferQueue,

Apache的包,里面有很多实用的类:

http://commons.apache.org/collections/

Google的包,里面有很多并发的牛B类:

AtomicLongMap,等等

大对象的数据结构:https://github.com/HugeCollections/Collections 

注意事项:

并发容器多数不能使用null值

 

Java之EnumSet与EnumMap

你可能感兴趣的:(JAVA语言)