EnumMap和EnumSet
1、EnumMap
如果需要一个Map的实现类,并且键的类型为枚举类型,可以使用HashMap,但应该使用一个专门的实现类EnumMap。主要是因为枚举类型有两个特征:一是它可能的值是有限的且预先定义的;二是枚举值都有一个顺序,这两个特征使得可以更为高效地实现Map接口。
1.1、基本用法
举个简单的例子。比如,有一批关于衣服的记录,我们希望按尺寸统计衣服的数量。定义一个简单的枚举类Size,表示衣服的尺寸:
java
public enum Size {
SMALL,
MEDIUM,
LARGE
}
定义一个简单类Clothes,表示衣服:
java
class Clothes {
String id;
Size size;
//省略getter/setter和构造方法
}
有一个表示衣服记录的列表List,我们希望按尺寸统计数量,统计方法可以为:
java
public static Map<Size, Integer> countBySize(List<Clothes> clothes) {
Map<Size, Integer> map = new EnumMap<Size, Integer>(Size.class);
for (Clothes c : clothes) {
Size size = c.getSize();
Integer count = map.get(size);
if (count != null) {
map.put(size, count + 1);
} else {
map.put(size, 1);
}
}
return map;
}
与HashMap不同,它需要传递一个类型信息,Size.class表示枚举类Size的运行时类型信息,Size.class也是一个对象,它的类型是Class。为什么需要这个参数呢?没有这个, EnumMap就不知道具体的枚举类是什么,也无法初始化内部的数据结构。
使用以上的统计方法也是很简单的,比如:
java
public static void main(String[] args) {
List<Clothes> cLothes = Arrays.asList(new Clothes[]{
new Clothes("C001", Size.SMALL),
new Clothes("C002", Size.MEDIUM),
new Clothes("C003", Size.LARGE)
});
System.out.println(countBySize(cLothes));
}
//{SMALL=1, MEDIUM=1, LARGE=1}
需要说明的是,与HashMap不同,EnumMap是保证顺序的,输出是按照键在枚举中的顺序的。
你可能认为,对于枚举,使用Map是没有必要的,比如对于上面的统计例子,可以使用一个简单的数组:
java
public static int[] countBySize(List<Clothes> clothes){
int[] stat = new int[Size.values().length];
for(Clothes c : clothes){
Size size = c.getSize();
stat[size.ordinal()]++;
}
return stat;
}
这个方法可以这么使用:
java
List<Clothes> clothes = Arrays.asList(new Clothes[]{
new Clothes("C001", Size.SMALL), new Clothes("C002", Size.LARGE),
new Clothes("C003", Size.LARGE), new Clothes("C004", Size.MEDIUM),
new Clothes("C005", Size.SMALL), new Clothes("C006", Size.SMALL),
});
int[] stat = countBySize(clothes);
for(int i=0; i<stat.length; i++){
System.out.println(Size.values()[i]+": "+ stat[i]);
}
输出为:
java
SMALL 3
MEDIUM 1
LARGE 2
可以达到同样的目的。但,直接使用数组需要自己维护数组索引和枚举值之间的关系,正如枚举的优点是简洁、安全、方便一样,EnumMap同样是更为简洁、安全、方便,它内部也是基于数组实现的,但隐藏了细节,提供了更为方便安全的接口。
1.2、实现原理
EnumMap有如下实例变量:
java
private final Class<K> keyType;
private transient K[] keyUniverse;
private transient Object[] vals;
private transient int size = 0;
keyType表示类型信息,keyUniverse表示键,是所有可能的枚举值,vals表示键对应的值,size表示键值对个数。EnumMap的基本构造方法代码为:
java
public EnumMap(Class<K> keyType) {
this.keyType = keyType;
keyUniverse = getKeyUniverse(keyType);
vals = new Object[keyUniverse.length];
}
调用了getKeyUniverse以初始化键数组,这段代码又调用了其他一些比较底层的代码,就不列举了,原理是最终调用了枚举类型的values方法,values方法返回所有可能的枚举值。
保存键值对的方法是put,代码为:
java
public V put(K key, V value) {
typeCheck(key);
int index = key.ordinal();
Object oldValue = vals[index];
vals[index] = maskNull(value);
if(oldValue == null)
size++;
return unmaskNull(oldValue);
}
首先调用typeCheck检查键的类型,其代码为:
java
private void typeCheck(K key) {
Class keyClass = key.getClass();
if(keyClass ! = keyType && keyClass.getSuperclass() ! = keyType)
throw new ClassCastException(keyClass + " ! = " + keyType);
}
如果类型不对,会抛出异常。如果类型正确,调用ordinal获取索引index,并将值value放入值数组vals[index]中。EnumMap允许值为null,为了区别null值与没有值,EnumMap将null值包装成了一个特殊的对象,有两个辅助方法用于null的打包和解包,打包方法为maskNull,解包方法为unmaskNull。这个特殊对象及两个方法的代码为:
java
private static final Object NULL = new Object() {
public int hashCode() {
return 0;
}
public String toString() {
return "java.util.EnumMap.NULL";
}
};
private Object maskNull(Object value) {
return (value == null ? NULL : value);
}
private V unmaskNull(Object value) {
return(V) (value == NULL ? null : value);
}
根据键获取值的方法是get,代码为:
java
public V get(Object key) {
return (isValidKey(key) ?
unmaskNull(vals[((Enum<?>)key).ordinal()]) : null);
}
如果键有效,通过ordinal方法取索引,然后直接在值数组vals里找。isValidKey的代码与typeCheck类似,但是返回boolean值而不是抛出异常,代码为:
java
private boolean isValidKey(Object key) {
if(key == null)
return false;
//Cheaper than instanceof Enum followed by getDeclaringClass
Class keyClass = key.getClass();
return keyClass == keyType || keyClass.getSuperclass() == keyType;
}
查看是否包含某个值的方法是containsValue,代码为:
java
public boolean containsValue(Object value) {
value = maskNull(value);
for(Object val : vals)
if(value.equals(val))
return true;
return false;
}
就是遍历值数组进行比较。
根据键删除的方法是remove,其代码为:
java
public V remove(Object key) {
if(! isValidKey(key))
return null;
int index = ((Enum)key).ordinal();
Object oldValue = vals[index];
vals[index] = null;
if(oldValue ! = null)
size--;
return unmaskNull(oldValue);
}
2、EnumSet
与EnumMap类似,之所以会有一个专门的针对枚举类型的实现类,主要是因为它可以非常高效地实现Set接口。
除了实现机制,EnumSet的用法也有一些不同。EnumSet可以说是处理枚举类型数据的一把利器,在一些应用领域,它非常方便和高效。
2.1、基本用法
与TreeSet/HashSet不同,EnumSet是一个抽象类,不能直接通过new新建,也就是说,类似下面代码是错误的:
java
EnumSet<Size> set = new EnumSet<Size>();
不过,EnumSet提供了若干静态工厂方法,可以创建EnumSet类型的对象,比如:
java
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType)
noneOf方法会创建一个指定枚举类型的EnumSet,不含任何元素。创建的EnumSet对象的实际类型是EnumSet的子类,待会我们再分析其具体实现。
为方便举例,我们定义一个表示星期几的枚举类Day,值从周一到周日,如下所示:
java
enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
可以这么用noneOf方法:
java
Set<Day> weekend = EnumSet.noneOf(Day.class);
weekend.add(Day.SATURDAY);
weekend.add(Day.SUNDAY);
System.out.println(weekend);
weekend表示休息日,noneOf返回的Set为空,添加了周六和周日,所以输出为:
[SATURDAY, SUNDAY]
EnumSet还有很多其他静态工厂方法,如下所示(省略了修饰public static):
java
//初始集合包括指定枚举类型的所有枚举值
<E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType)
//初始集合包括枚举值中指定范围的元素
<E extends Enum<E>> EnumSet<E> range(E from, E to)
//初始集合包括指定集合的补集
<E extends Enum<E>> EnumSet<E> complementOf(EnumSet<E> s)
//初始集合包括参数中的所有元素
<E extends Enum<E>> EnumSet<E> of(E e)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4, E e5)
<E extends Enum<E>> EnumSet<E> of(E first, E... rest)
//初始集合包括参数容器中的所有元素
<E extends Enum<E>> EnumSet<E> copyOf(EnumSet<E> s)
<E extends Enum<E>> EnumSet<E> copyOf(Collection<E> c)
可以看到,EnumSet有很多重载形式的of方法,最后一个接受的是可变参数,其他重载方法看上去是多余的,之所以有其他重载方法是因为可变参数的运行效率低一些。
2.2、应用场景
在一些工作中(如医生、客服),不是每个工作人员每天都在的,每个人可工作的时间是不一样的,比如张三可能是周一和周三,李四可能是周四和周六,给定每个人可工作的时间,我们可能有一些问题需要回答。比如:
- 有没有哪天一个人都不会来?
- 有哪些天至少会有一个人来?
- 有哪些天至少会有两个人来?
- 有哪些天所有人都会来,以便开会?
- 哪些人周一和周二都会来?
使用EnumSet,可以方便高效地回答这些问题,怎么做呢?我们先来定义一个表示工作人员的类Worker,如下所示:
java
class Worker {
String name;
Set<Day> availableDays;
public Worker(String name, Set<Day> availableDays) {
this.name = name;
this.availableDays = availableDays;
}
//省略getter方法
}
为演示方便,将所有工作人员的信息放到一个数组workers中,如下所示:
java
Worker[] workers = new Worker[]{
new Worker("张三", EnumSet.of(
Day.MONDAY, Day.TUESDAY, Day.WEDNESDAY, Day.FRIDAY)),
new Worker("李四", EnumSet.of(
Day.TUESDAY, Day.THURSDAY, Day.SATURDAY)),
new Worker("王五", EnumSet.of(Day.TUESDAY, Day.THURSDAY)),
};
每个工作人员的可工作时间用一个EnumSet表示。有了这个信息,我们就可以回答以上的问题了。哪些天一个人都不会来?代码可以为:
java
Set<Day> days = EnumSet.allOf(Day.class);
for(Worker w : workers){
days.removeAll(w.getAvailableDays());
}
System.out.println(days);
days初始化为所有值,然后遍历workers,从days中删除可工作的所有时间,最终剩下的就是一个人都不会来的时间,这实际是在求worker时间并集的补集,输出为:
java
[SUNDAY]
有哪些天至少会有一个人来?就是求worker时间的并集,代码可以为:
java
Set<Day> days = EnumSet.noneOf(Day.class);
for(Worker w : workers){
days.addAll(w.getAvailableDays());
}
System.out.println(days);
输出为:
java
[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY]
有哪些天所有人都会来?就是求worker时间的交集,代码可以为:
java
Set<Day> days = EnumSet.allOf(Day.class);
for(Worker w : workers){
days.retainAll(w.getAvailableDays());
}
System.out.println(days);
输出为:
[TUESDAY]
哪些人周一和周二都会来?使用containsAll方法,代码可以为:
java
Set<Worker> availableWorkers = new HashSet<Worker>();
for(Worker w : workers){
if(w.getAvailableDays().containsAll(
EnumSet.of(Day.MONDAY, Day.TUESDAY))){
availableWorkers.add(w);
}
}
for(Worker w : availableWorkers){
System.out.println(w.getName());
}
输出为:
java
张三
哪些天至少会有两个人来?我们先使用EnumMap统计每天的人数,然后找出至少有两个人的天,代码可以为:
java
Map<Day, Integer> countMap = new EnumMap<>(Day.class);
for(Worker w : workers){
for(Day d : w.getAvailableDays()){
Integer count = countMap.get(d);
countMap.put(d, count==null?1:count+1);
}
}
Set<Day> days = EnumSet.noneOf(Day.class);
for(Map.Entry<Day, Integer> entry : countMap.entrySet()){
if(entry.getValue()>=2){
days.add(entry.getKey());
}
}
System.out.println(days);
输出为:
java
[TUESDAY, THURSDAY]
2.3、实现原理
EnumSet是使用位向量实现的,什么是位向量呢?就是用一个位表示一个元素的状态,用一组位表示一个集合的状态,每个位对应一个元素,而状态只可能有两种。
对于之前的枚举类Day,它有7个枚举值,一个Day的集合就可以用一个字节byte表示,最高位不用,设为0,最右边的位对应顺序最小的枚举值,从右到左,每位对应一个枚举值,1表示包含该元素,0表示不含该元素。
比如,表示包含Day.MONDAY、Day.TUESDAY、Day.WEDNESDAY、Day.FRIDAY的集合,位向量结构如图所示。

对应的整数是23。
位向量能表示的元素个数与向量长度有关,一个byte类型能表示8个元素,一个long类型能表示64个元素,那EnumSet用的长度是多少呢?
EnumSet是一个抽象类,它没有定义使用的向量长度,它有两个子类:RegularEnumSet和JumboEnumSet。RegularEnumSet使用一个long类型的变量作为位向量,long类型的位长度是64,而JumboEnumSet使用一个long类型的数组。如果枚举值个数小于等于64,则静态工厂方法中创建的就是RegularEnumSet,如果大于64就是JumboEnumSet。
理解了位向量的基本概念,下面我们来看EnumSet的实现,包括其内部组成和一些主要方法的实现。同EnumMap一样,EnumSet也有表示类型信息和所有枚举值的实例变量,如下所示:
java
final Class<E> elementType;
final Enum[] universe;
elementType表示类型信息,universe表示枚举类的所有枚举值。
EnumSet自身没有记录元素个数的变量,也没有位向量,它们是子类维护的。对于RegularEnumSet,它用一个long类型表示位向量,代码为:
java
private long elements = 0L;
它没有定义表示元素个数的变量,是实时计算出来的,计算的代码是:
java
public int size() {
return Long.bitCount(elements);
}
对于JumboEnumSet,它用一个long数组表示,有单独的size变量,代码为:
java
private long elements[];
private int size = 0;
我们来看EnumSet的静态工厂方法noneOf,代码为:
java
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);
}
getUniverse的代码与EnumMap是一样的,就不赘述了。如果元素个数不超过64,就创建RegularEnumSet,否则创建JumboEnumSet。
RegularEnumSet和JumboEnumSet的构造方法为:
java
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的构造方法,其代码为:
java
EnumSet(Class<E>elementType, Enum[] universe) {
this.elementType = elementType;
this.universe = universe;
}
就是给实例变量赋值,JumboEnumSet根据元素个数分配足够长度的long数组。
其他工厂方法基本都是先调用noneOf方法构造一个空的集合,然后再调用添加方法。我们来看添加方法,RegularEnumSet的add方法的代码为:
java
public boolean add(E e) {
typeCheck(e);
long oldElements = elements;
elements |= (1L << ((Enum)e).ordinal());
return elements ! = oldElements;
}
主要代码是按位或操作:
java
elements |= (1L << ((Enum)e).ordinal());
(1L << ((Enum)e).ordinal())将元素e对应的位设为1,与现有的位向量elements相或,就表示添加e了。JumboEnumSet的add方法的代码为:
java
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就表示数组索引,有了索引之后,其他操作与Regular-EnumSet就类似了。
对于其他操作,JumboEnumSet的思路是类似的,主要算法与RegularEnumSet一样,主要是增加了寻找对应long位向量的操作,或者有一些循环处理,逻辑也都比较简单,后文就只介绍RegularEnumSet的实现了。
RegularEnumSet的remove方法的代码为:
java
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;
}
主要代码是:
java
elements &= ~(1L << ((Enum)e).ordinal());
~是取反,该代码将元素e对应的位设为了0,这样就完成了删除。
查看是否包含某元素的方法是contains,其代码为:
java
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,则表示包含。
EnumSet的静态工厂方法complementOf是求补集,它调用的代码是:
java
void complement() {
if(universe.length ! = 0) {
elements = ~elements;
elements &= -1L >>> -universe.length; // Mask unused bits
}
}
这段代码有点晦涩,elements=~elements比较容易理解,就是按位取反,相当于就是取补集,但我们知道elements是64位的,当前枚举类可能没有用那么多位,取反后高位部分都变为了1,需要将超出universe.length的部分设为0。下面的代码就是在做这件事:
java
elements &= -1L >>> -universe.length;
-1L是64位全1的二进制,我们在剖析Integer一节介绍过移动位数是负数的情况,上面代码相当于:
java
elements &= -1L >>> (64-universe.length);
如果universe.length为7,则-1L>>>(64-7)就是二进制的1111111,与elements相与,就会将超出universe.length部分的右边的57位都变为0。