Java 之EnumSet与EnumMap
EnumSet是Java枚举类型的泛型容器,Java既然有了SortedSet、TreeSet、HashSet等容器,为何还要多一个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
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.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
实现注意事项:所有基本操作都在固定时间内执行。虽然并不保证,但它们很可能比其 HashSet 副本更快。
如果参数是另一个 EnumSet 实例,则诸如 addAll() 和 AbstractSet.removeAll(java.util.Collection) 之类的批量操作
也会在固定时间内执行。
注意1:不允许使用 null 元素。试图插入 null 元素将抛出 NullPointerException。但是,
试图测试是否出现 null 元素或移除 null 元素将不会抛出异常。
注意2:EnumSet是不同步的。不是线程安全的。
注意3:EnumSet的本质就为枚举类型定制的一个Set,且枚举set中所有元素都必须来自单个枚举类型。
注意4:关于EnumSet的存储,文档中提到是这样的。 “枚举 set 在内部表示为位向量。”
我想它应该是用一个bit为来表示的于之对应的枚举变量是否在集合中。
比如:0x1001
假如约定从低位开始,就表示第0个,和第三个枚举类型变量在EnumSet中。
这样的话空间和时间性能也就应该很好。
注意5:至于Enum的枚举常量的位置(序数)可以用Enum的ordinal()方法得到。
注意6:在jdk内部可以一个数组的形式一个枚举的枚举常量。
下面是来自来JDK中RegularEnumSet.java的一个示例
private static
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
实现类:
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
实现类:
ArrayBlockingQueue, ArrayDeque, ConcurrentLinkedQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, LinkedList, PriorityBlockingQueue, PriorityQueue, SynchronousQueue
Set
子接口:
NavigableSet
实现类:
ConcurrentSkipListSet, CopyOnWriteArraySet, EnumSet, HashSet, LinkedHashSet, TreeSet
Map
子接口:
ConcurrentMap
实现类:
ConcurrentHashMap, ConcurrentSkipListMap, EnumMap, HashMap, Hashtable, IdentityHashMap, LinkedHashMap, TreeMap, WeakHashMap
并发与线程安全等
通常含有Concurrent,CopyOnWrite,Blocking的是线程安全的,但是这些线程安全通常是有条件的,所以在使用前一定要仔细阅读文档。
具体实现:
List
ArrayList
CopyOnWriteArrayList
只保证历遍操作是线程安全的,get操作并不保证,也就是说如果先得到size,再调用get(size-1),有可能会失效
那么CopyOnWriteArrayList是如何实现线程安全的迭代操作?
在迭代器中保存原数组。
LinkedList
Vector
Stack
Queue
LinkedList
ArrayDeque
PriorityQueue
PriorityBlockingQueue
ArrayBlockingQueue
ConcurrentLinkedQueue
DelayQueue
LinkedBlockingDeque
LinkedBlockingQueue
SynchronousQueue
Deque
ArrayDeque
LinkedList
LinkedBlockingDeque
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
EnumMap,TODO
ConcurrentHashMap,参考之前的文章
ConcurrentSkipListMap,TODO,log(n)的时间复杂度,有点像多级链表保存的,貌似有点像redis中的SortedSet的实现
Hashtable,过时
IdentityHashMap,正常的HashMap中比较是用equals方法,这个用的是“==”比较符
WeakHashMap
其它的一些实用的第三方的数据结构:
LRUCache,LongHashMap,Java7中的LinkedTransferQueue,
Apache的包,里面有很多实用的类:
http://commons.apache.org/collections/
Google的包,里面有很多并发的牛B类:
AtomicLongMap,等等
大对象的数据结构:https://github.com/HugeCollections/Collections
注意事项:
并发容器多数不能使用null值
Java之EnumSet与EnumMap