10.7 剖析EnumMap
10.7 剖析EnumMap
如果需要一个Map的实现类,并且键的类型为枚举类型,可以使用HashMap,但应该使用一个专门的实现类EnumMap。为什么要有一个专门的类呢?我们之前介绍过枚举的本质,主要是因为枚举类型有两个特征:一是它可能的值是有限的且预先定义的;二是枚举值都有一个顺序,这两个特征使得可以更为高效地实现Map接口。我们先来看EnumMap的用法,然后看它到底是怎么实现的。
10.7.1 基本用法
举个简单的例子。比如,有一批关于衣服的记录,我们希望按尺寸统计衣服的数量。定义一个简单的枚举类Size,表示衣服的尺寸:
1 | public enum Size { |
定义一个简单类Clothes,表示衣服:
1 | class Clothes { |
有一个表示衣服记录的列表List<Clothes>
,我们希望按尺寸统计数量,统计方法可以为:
1 | public static Map<Size, Integer> countBySize(List<Clothes> clothes){ |
大部分代码都很简单,需要注意的是EnumMap的构造方法,如下所示:
1 | Map<Size, Integer> map = new EnumMap<>(Size.class); |
与HashMap不同,它需要传递一个类型信息,Size.class表示枚举类Size的运行时类型信息,Size.class也是一个对象,它的类型是Class。为什么需要这个参数呢?没有这个, EnumMap就不知道具体的枚举类是什么,也无法初始化内部的数据结构。
使用以上的统计方法也是很简单的,比如:
1 | List<Clothes> clothes = Arrays.asList(new Clothes[]{ |
输出为:
1 | {SMALL=3, MEDIUM=1, LARGE=2} |
需要说明的是,与HashMap不同,EnumMap是保证顺序的,输出是按照键在枚举中的顺序的。
你可能认为,对于枚举,使用Map是没有必要的,比如对于上面的统计例子,可以使用一个简单的数组:
1 | public static int[] countBySize(List<Clothes> clothes){ |
这个方法可以这么使用:
1 | List<Clothes> clothes = Arrays.asList(new Clothes[]{ |
输出为:
1 | SMALL 3 |
可以达到同样的目的。但,直接使用数组需要自己维护数组索引和枚举值之间的关系,正如枚举的优点是简洁、安全、方便一样,EnumMap同样是更为简洁、安全、方便,它内部也是基于数组实现的,但隐藏了细节,提供了更为方便安全的接口。
10.7.2 实现原理
下面我们来看下具体的代码(基于Java
7)。从内部组成开始。EnumMap有如下实例变量:
1 | private final Class<K> keyType; |
keyType表示类型信息,keyUniverse表示键,是所有可能的枚举值,vals表示键对应的值,size表示键值对个数。EnumMap的基本构造方法代码为:
1 | public EnumMap(Class<K> keyType) { |
调用了getKeyUniverse以初始化键数组,这段代码又调用了其他一些比较底层的代码,就不列举了,原理是最终调用了枚举类型的values方法,values方法返回所有可能的枚举值。关于values方法,我们在枚举一节介绍过其用法和实现原理,这里就不赘述了。
保存键值对的方法是put,代码为:
1 | public V put(K key, V value) { |
首先调用typeCheck检查键的类型,其代码为:
1 | private void typeCheck(K key) { |
如果类型不对,会抛出异常。如果类型正确,调用ordinal获取索引index,并将值value放入值数组vals[index]中。EnumMap允许值为null,为了区别null值与没有值,EnumMap将null值包装成了一个特殊的对象,有两个辅助方法用于null的打包和解包,打包方法为maskNull,解包方法为unmaskNull。这个特殊对象及两个方法的代码为:
1 | private static final Object NULL = new Object() { |
根据键获取值的方法是get,代码为:
1 | public V get(Object key) { |
如果键有效,通过ordinal方法取索引,然后直接在值数组vals里找。isValidKey的代码与typeCheck类似,但是返回boolean值而不是抛出异常,代码为:
1 | private boolean isValidKey(Object key) { |
查看是否包含某个值的方法是containsValue,代码为:
1 | public boolean containsValue(Object value) { |
就是遍历值数组进行比较。
根据键删除的方法是remove,其代码为:
1 | public V remove(Object key) { |
代码也很简单,就不解释了。
10.7.3 小结
本节介绍了EnumMap的用法和实现原理,用法上,如果需要一个Map且键是枚举类型,则应该用它,简洁、方便、安全;实现原理上,内部有两个数组,长度相同,一个表示所有可能的键,一个表示对应的值,值为null表示没有该键值对,键都有一个对应的索引,根据索引可直接访问和操作其键和值,效率很高。
下一节,我们来看枚举类型的Set接口的实现类EnumSet,与之前介绍的Set的实现类不同,它内部没有用对应的Map类EnumMap,而是使用了一种极为高效的方式,什么方式呢?