到本文为止,之前已经介绍过HashMap、TreeMap、LinkedHashMap,本篇文章介绍一下集合框架系列最后一种类型的Map——EnumMap。在之前的文章中,我曾介绍过Java API中枚举类的本质(Java编程拾遗『枚举类』),本文讲的EnumMap就是一种key为枚举类型的Map结构。EnumMap类的继承关系如下:
Enum类继承了AbstractMap抽象类,在EnumMap对相应方法做了特别的实现,保证EnumMap key为特定枚举类型,键值对按照枚举类定义的顺序有序。
1. 调用示例
public enum SizeEnum {
SMALL, MEDIUM, LARGE, EXTRA_LARGE
}
@AllArgsConstructor
@Getter
@Setter
public class Clothe {
Long id;
SizeEnum size;
}
public class EnumMapTest {
public static void main(String[] args) {
List<Clothe> clothes = Lists.newArrayList();
clothes.add(new Clothe(1L, SizeEnum.LARGE));
clothes.add(new Clothe(2L, SizeEnum.EXTRA_LARGE));
clothes.add(new Clothe(3L, SizeEnum.SMALL));
clothes.add(new Clothe(4L, SizeEnum.SMALL));
Map<SizeEnum, Integer> map = new EnumMap<>(SizeEnum.class);
for (Clothe c : clothes) {
map.compute(c.getSize(), (k, v) -> (v == null) ? 1 : ++v);
}
System.out.println(map);
}
}
统计Clothe List中,各个Size的数目,运行结果如下:
{SMALL=2, LARGE=1, EXTRA_LARGE=1}
可以看到,EnumMap各键值对之间的顺序跟枚举类定义的顺序一致。
2. 方法说明
2.1 构造函数
S.N. | 方法 | 说明 |
---|---|---|
1 | public EnumMap(Class<K> keyType) | 声明key类型为keyType的EnumMap |
2 | public EnumMap(EnumMap<K, ? extends V> m) | 通过EnumMap对象初始化一个EnumMap对象 |
3 | public EnumMap(Map<K, ? extends V> m) | 通过Map对象初始化一个EnumMap对象,如果Map非EnumMap,则要求key类型为Enum类 |
2.2 Map接口
除了构造函数之外的方法,全部继承自Map接口,如下:
S.N. | 方法 | 说明 |
---|---|---|
1 | void clear() | 清除Map中所有的KV映射 |
2 | default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) | Java 8新方法,如果key在Map中不存在,则根据Function规则计算key对应的value值, 并进行put操作(前提计算得到的value值也不为null) |
3 | default V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) |
Java 8新方法,如果key在Map中存在,则根据BiFunction规则通过key和对应的value 计算newValue,如果newValue为null,则删除key对应的键值对,否则替换key对应的value值 |
4 | default V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) | Java 8新方法,相当于上述两个方法的综合,根据BiFunction,通过key和对应的value 计算newValue,如果newValue为null,会删除对应KV映射,否则会插入或替换原KV映射 |
5 | boolean containsKey(Object key) | 判断Map中是否存在key键 |
6 | boolean containsValue(Object value) | 判断Map中是否存在value值 |
7 | Set<Map.Entry<K, V>> entrySet() | 将Map所有的键值对转换为Set集合 |
8 | boolean equals(Object o) | equals方法 |
9 | default void forEach(BiConsumer<? super K, ? super V> action) | Java 8新方法,遍历Map,通过BiConsumer处理Map中每一个键值对的key、value |
10 | V get(Object key) | 获取Map中key对应的value |
11 | default V getOrDefault(Object key, V defaultValue) | Java 8新方法,获取Map中key对应的value,如果key不存在,则返回defaultValue |
12 | int hashCode() | hashCode方法 |
13 | boolean isEmpty() | 判断Map是否为空 |
14 | Set<K> keySet() | 将Map中所有的key转化为Set集合 |
15 | default V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) |
如果key在Map中不存在或者对应的值为null,则给其一个对应的非null值value。否则根据 BiFunction规则通过oldValue和value计算一个newValue,替换或删除(newValue为null)原 键值对 |
16 | V put(K key, V value); | 为Map添加一个键值对 |
17 | void putAll(Map<? extends K, ? extends V> m) | 将另一个Map的所有键值对添加到该Map中 |
18 | default V putIfAbsent(K key, V value) | Java 8新方法,如果Map中key不存在,则添加一个key-value键值对 |
19 | V remove(Object key) | 从Map中移除key对应的键值对 |
20 | default boolean remove(Object key, Object value) | Java8新方法,删除key-value键值对 |
21 | default V replace(K key, V value) | Java8新方法,将Map中key对应的值替换为value |
22 | default boolean replace(K key, V oldValue, V newValue) | Java8新方法,将Map中key-oldValue键值对的value值替换为newValue |
23 | default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) | Java8新方法,将Map中所有的键值对的value,替换为根据BiFunction规则通过key-value 得到的新值 |
24 | int size() | 返回Map中EntrySet的数目 |
25 | Collection<V> values() | 返回Map中所有value组成的集合 |
3. 实现源码分析
3.1 EnumMap类定义
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
implements java.io.Serializable, Cloneable
{
private final Class<K> keyType;
private transient K[] keyUniverse;
private transient Object[] vals;
private transient int size = 0;
//methods
}
- keyType:EnumMap key对应的枚举类型
- keyUniverse:EnumMap key对应的枚举类型的所有枚举值组成的数组,顺序跟枚举类中枚举值定义顺序一致
- vals:EnumMap中所有key对应的value值组成的数组,顺序跟枚举类中枚举值定义顺序一致
- size:EnumMap中有效键值对数目
比如上面示例中,EnumMap为:
{SMALL=2, LARGE=1, EXTRA_LARGE=1}
底层存储如下图所示:
3.2 构造函数
public EnumMap(Class<K> keyType) {
this.keyType = keyType;
keyUniverse = getKeyUniverse(keyType);
vals = new Object[keyUniverse.length];
}
构造函数,传入EnumMap key类型,构造函数中完成将传入的keyType赋值给EnumMap成员变量keyType,调用getKeyUniverse方法,将keyType对应的枚举类的所有枚举值组成的数组赋值给keyUniverse,vals初始化为跟枚举值数量相同的Object数组。
public EnumMap(EnumMap<K, ? extends V> m) {
keyType = m.keyType;
keyUniverse = m.keyUniverse;
vals = m.vals.clone();
size = m.size;
}
通过EnumMap对象构造EnumMap,通过对成员变量直接赋值实现。
public EnumMap(Map<K, ? extends V> m) {
if (m instanceof EnumMap) {
EnumMap<K, ? extends V> em = (EnumMap<K, ? extends V>) m;
keyType = em.keyType;
keyUniverse = em.keyUniverse;
vals = em.vals.clone();
size = em.size;
} else {
if (m.isEmpty())
throw new IllegalArgumentException("Specified map is empty");
keyType = m.keySet().iterator().next().getDeclaringClass();
keyUniverse = getKeyUniverse(keyType);
vals = new Object[keyUniverse.length];
putAll(m);
}
}
通过Map对象构造EnumMap对象,这里分为两种情况,如果Map对象是一个EnumMap示例,则跟上面的构造函数实现一样。否则,则要求Map的key类型为枚举类型,通过Map的key获取keyType,通过上面构造函数1,初始化EnumMap,然后调用putAll方法,将Map中所有的键值对添加到EnumMap中。
3.3 put方法
public V put(K key, V value) {
//检查待插入key类型是否为keyType
typeCheck(key);
//获取枚举值key在枚举类中对应的ordinal(序号)
int index = key.ordinal();
//取key在value数组中对应的值
Object oldValue = vals[index];
//将新的value写入value数组对应index位置
vals[index] = maskNull(value);
//如果oldValue为null,说明key在EnumMap中不存在,将size自增1
if (oldValue == null)
size++;
//返回oldValue值
return unmaskNull(oldValue);
}
maskNull和unmaskNull方法用于对null进行包装和反包装。可以发现,vals数组中null元素只有可能来自构造函数,一旦进行put操作,即使带插入的键值对value为null,插入之后,vals对应index位置也不是null了。
private Object maskNull(Object value) {
return (value == null ? NULL : value);
}
private V unmaskNull(Object value) {
return (V)(value == NULL ? null : value);
}
NULL为一个静态的Object对象,如下:
private static final Object NULL = new Object() {
public int hashCode() {
return 0;
}
public String toString() {
return "java.util.EnumMap.NULL";
}
};
3.4 remove方法
public V remove(Object key) {
//检查待插入key类型是否为keyType
if (!isValidKey(key))
return null;
//获取枚举值key在枚举类中对应的ordinal(序号)
int index = ((Enum<?>)key).ordinal();
//获取key在value数组中对应的值
Object oldValue = vals[index];
//将index索引位置的value元素
vals[index] = null;
//如果oldValue不为null,说明index索引位置之前插入过元素,现在做remove操作后,将size自减
if (oldValue != null)
size--;
//对oldValue反包装
return unmaskNull(oldValue);
}
3.4 get方法
//获取EnumMap中键key对应的value
//如果key是EnumMap中合法的key,则返回vals数组中对应index的值,否则返回null
public V get(Object key) {
return (isValidKey(key) ?
unmaskNull(vals[((Enum<?>)key).ordinal()]) : null);
}
isValidKey方法如下:
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;
}
3.5 containsKey方法
//判断key是否存在,条件:key合法 && vals数组对应位置不为null
public boolean containsKey(Object key) {
return isValidKey(key) && vals[((Enum<?>)key).ordinal()] != null;
}
3.6 containsValue方法
//判断value是否存在
public boolean containsValue(Object value) {
//对value进行包装
value = maskNull(value);
//遍历vals数组,判断value是否存在
for (Object val : vals)
if (value.equals(val))
return true;
return false;
}
4. EnumMap特点
EnumMap的key为枚举类型,为了便于更好的理解EnumMap,搞清楚枚举类的本质还是很重要的(Java编程拾遗『枚举类』)。
- EnumMap内部有两个数组,长度相同,分别用来表示key对应的枚举类所有的枚举值,和所有key对应的value,由于枚举类的特性,根据枚举值可以获取枚举值对应的ordinal,进而获取key和value在EnumMap内部数组的存储位置,所以效率很高,get、put、remove、containsKey等操作时间复杂度都为n(1)
- EnumMap可以保证键值对之间按照枚举值有序
参考链接:
- Java API源码