1 简介
我们知道 Map 只是一个接口, 它有多种实现, Java 中最常用的是 HashMap 了. 而本文想讲述的是另一个实现: EnumMap. 它是枚举类型的 Map, 要求它的 Key 值都必须是枚举型的.
2 创建你的 EnumMap
既然是关于枚举类型的 Map, 我们先创建一个枚举, 以便后续使用:
- public enum Directions {
- NORTH, SOUTH, EAST, WEST
- }
2.1 创建 EnumMap 的三种方法
JDK 提供的创建 EnumMap 的方法有三种, 代码如下:
- //new EnumMap
- EnumMap<Direction, String> enumMap = new EnumMap<>(Direction.class);
- enumMap.put(Direction.EAST, "东");
- enumMap.put(Direction.SOUTH, "南");
- // 从 EnumMap 复制
- EnumMap<Direction, String> enumMapCopyEnumMap = new EnumMap<>(enumMap);
- assertEquals(enumMap, enumMapCopyEnumMap);
- // 从 Map 复制
- Map<Direction, String> hashMap = Maps.newHashMap();
- hashMap.put(Direction.EAST, "东");
- hashMap.put(Direction.SOUTH, "南");
- EnumMap<Direction, String> enumMapCopyHashMap = new EnumMap<>(hashMap);
- assertEquals(enumMap, enumMapCopyHashMap);
(1) 使用 new EnumMap() 方法时, 与 HashMap 不同, 它必须传入一个枚举的类型才能创建对象;
(2) 从 EnumMap 复制, 这时传入的参数为 EnumMap;
(3) 从 Map 复制, 传入的参数为 Map, 但要求 Key 的类型必须是枚举型.
2.2 聪明的 Guava
其实可以综合上面三种情况, 实际就是两种方法:
(1) 使用 new EnumMap(Class<K> keyType)
(2) 使用 new EnumMap(Map<K, ? extends V> m)
聪明的 Guava 就只提供了这两种方法, 如下:
- // 使用 Guava 创建
- EnumMap<Direction, String> enumMapGuava = Maps.newEnumMap(Direction.class);
- enumMapGuava.put(Direction.SOUTH, "南");
- assertEquals(1, enumMapGuava.size());
- enumMapGuava = Maps.newEnumMap(enumMap);
- assertEquals(enumMap, enumMapGuava);
3 基本操作
提供的方法与 Map 当然是一样的, 操作十分方便, 代码如下:
- @Test
- public void operations() {
- EnumMap<Direction, String> map = Maps.newEnumMap(Direction.class);
- // 增加
- map.put(Direction.EAST, "东");
- map.put(Direction.SOUTH, "南");
- map.put(Direction.WEST, "西");
- // 查询
- assertTrue(map.containsKey(Direction.EAST));
- assertFalse(map.containsKey(Direction.NORTH));
- // 删除
- map.remove(Direction.EAST);
- assertFalse(map.containsKey(Direction.EAST));
- assertFalse(map.remove(Direction.WEST, "北"));
- assertTrue(map.remove(Direction.WEST, "西"));
- // 清空
- map.clear();
- assertEquals(0, map.size());
- }
需要特别指出的是删除方法, 可以传入 Key 和 Value 两个参数, map.remove(Direction.WEST, "西") 当键值对匹配时, 则可以删除成功; map.remove(Direction.WEST, "北") 匹配失败, 则不会删除.
4 集合视图
4.1 有序性
与 Map 接口提供的功能一样, EnumMap 也能返回它的所有 Values,Keys 和 Entry 等. 但与 HashMap 不同的是, EnumMap 返回的视图是有序的, 这个顺序不是插入的顺序, 而是枚举定义的顺序. 代码如下:
- EnumMap<Direction, String> map = Maps.newEnumMap(Direction.class);
- map.put(Direction.EAST, "东");
- map.put(Direction.SOUTH, "南");
- map.put(Direction.WEST, "西");
- map.put(Direction.NORTH, "北");
- // 返回所有 Value
- Collection<String> values = map.values();
- values.forEach(System.out::println);
- // 返回所有 Key
- Set<Direction> keySet = map.keySet();
- keySet.forEach(System.out::println);
- // 返回所有 < Key,Value>
- Set<Map.Entry<Direction, String>> entrySet = map.entrySet();
- entrySet.forEach(entry -> {
- System.out.println(entry.getKey() + ":" + entry.getValue());
- });
输出的结果如下:
北
南
东
西
- NORTH
- SOUTH
- EAST
- WEST
NORTH: 北
SOUTH: 南
EAST: 东
WEST: 西
这个顺序与我们定义枚举的顺序确实是一样的, 而与添加的顺序无关.
4.2 联动性
除了有序性之外, EnumMap 返回的集合视图还有一点不同就是联动性, 即牵一发而动全身. 改变其中一个, 另外的也跟着变了. 看代码一下就明白了:
- //Values,keySet,entrySet 改变会影响其它
- values.remove("东");
- assertEquals(3, map.size());
- assertEquals(3, keySet.size());
- assertEquals(3, entrySet.size());
- keySet.remove(Direction.WEST);
- assertEquals(2, map.size());
- assertEquals(2, values.size());
- assertEquals(2, entrySet.size());
- entrySet.removeIf(entry -> Objects.equals(entry.getValue(), "北"));
- assertEquals(1, map.size());
- assertEquals(1, keySet.size());
- assertEquals(1, values.size());
- //Map 的改变会影响其它视图
- map.clear();
- assertEquals(0, values.size());
- assertEquals(0, keySet.size());
- assertEquals(0, entrySet.size());
5 性能
性能是我们选择 EnumMap 的主要原因之一, 那为何它性能会比优秀的 HashMap 还要好呢? 通过看源码可以得知:
(1) 底层是通过两个数组来存放数据的, 一个放 Keys, 一个放 Values;
(2) 因为 Key 值是枚举类型, 即一开始就确定了元素个数, 所以在创建一个 EnumMap 的时候, 存放数据的数组就已经确定了大小, 不用考虑后续扩容带来的性能问题.
(3) 枚举本身就是固定顺序的, 可以通过 Enum.ordinal() 方法获得顺序, 这个便可以作为查询与插入的索引, 而不用计算 HashCode, 性能也会比较快. 这个顺序也就是数组下标. 这也是 EnumMap 的集合视图都是有序的原因.
(4) 因为大小固定, 则不用考虑加载因子, 也不会有哈希冲突的问题, 空间复杂度小.
6 结论
本文介绍了 EnumMap 作为一个 Map 的特殊实现的创建, 使用, 集合视图和性能分析, 发现它的确是有过人之处的. 当我们的 Key 值是枚举时, 不妨可以试一试 EnumMap, 性能会更好哦.