0%

Java Map 和 Set(七):剖析 EnumMap

1. EnumMap 的基本概念

  • 如果需要一个 Map 的实现类,并且键的类型为枚举类型,可以使用 HashMap,但应该使用一个专门的实现类 EnumMap
  • EnumMap 内部基于两个数组实现

2. 针对 Map 的类型为枚举类型,为什么要有一个专门的 Map 的实现类

  • 主要是因为枚举类型有两个主要特征

    • 枚举可能的值是有限的且预先定义的
    • 枚举值都有一个顺序
  • 这两个特征使得可以更为高效地实现 Map 接口

3. 有一批关于衣服的纪录,我们希望按尺寸统计衣服的数量,请用数组和 EnumMap 分别实现,并分析这两种实现的区别(表示衣服尺寸的枚举类和表示衣服的实体类以及衣服列表分别如下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//表示衣服尺寸的枚举类
public enum Size {
SMALL, MEDIUM, LARGE
}

//表示衣服的实体类
class Clothes {
String id;
Size size;
//省略 getter、setter 和构造方法
}

//衣服列表
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)
});
  • 代码

    • 数组实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      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;
      }

      //使用 int[] countBySize() 方法
      int[] stat = countBySize(clothes);
      for(int i = 0; i < stat.length; i++) {
      System.out.println(Size.values()[i] + ": " + stat[i]);
      }

      //输出
      SMALL: 3
      MEDIUM: 1
      LARGE: 2
    • EnumMap 实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      public static Map<Size, Integer> countBySize(List<Clothes> clothes) {
      Map<Size, Integer> map = new EnumMap<> (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, count);
      }
      }
      return map;
      }

      //使用 Map<Size, Integer> countBySize() 方法
      System.out.println(countBySize(clothes));

      //输出
      {SMALL=3, MEDIUM=1, LARGE=2}
  • 数组实现中:直接使用数组需要自己维护数组索引和枚举值之间的关系

  • EnumMap 实现中:EnumMap 的构造方法与 HashMap 不同,它需要传递一个类型信息

    • Size.class 表示枚举类 Size 的运行时类型信息,Size.class 也是一个对象,它的类型是 Class
    • 之所以需要这个参数,是因为如果没有的话,EnumMap 就不知道具体的枚举类型是什么,也无法初始化内部的数据结构
    • HashMap 不同,EnumMap 是保证顺序的,输出是按照键在枚举中的顺序
  • 区别:正如枚举的优点是简洁、安全、方便一样,EnumMap 同样是更为简洁、安全、方便,它内部也是基于数组实现的,但隐藏了细节,提供了更为安全的接口

4. EnumMap 的内部组成

  • EnumMap 有如下实例变量

    1
    2
    3
    4
    private final Class<K> keyType; //表示类型信息
    private transient K[] keyUniverse; //表示键,是所有可能的枚举值
    private transient Object[] vals; //表示键对应的值
    private transient int size = 0; //表示键值对个数
  • EnumMap 的构造方法代码

    1
    2
    3
    4
    5
    public EnumMap(Class<K> keyType) {
    this.keyType = keyType;
    keyUniverse = getKeyUniverse(keyType);
    vals = new Object[keyUniverse.length];
    }
    • 调用了 getKeyUniverse() 以初始化键数组,这段代码又调用了其他一些比较底层的代码,就不列举了
    • 原理是最终调用了枚举类型的 values() 方法,values() 方法返回所有可能的枚举值
  • EnumMap 允许值为 null,为了区别 null 值与没有值,EnumMapnull 值包装成了一个特殊的对象,有两个辅助方法用于 null 的打包和解包

    • null 值的包装对象

      1
      2
      3
      4
      5
      6
      7
      8
      private static final Object NULL = new Object() {
      public int hashCode() {
      return 0;
      }
      public String toString() {
      return "java.util.EnumMap.NULL";
      }
      }
    • 打包方法 maskNull()

      1
      2
      3
      private Object maskNull(Object value) {
      return (value == null ? NULL : value);
      }
    • 解包方法 unmaskNull()

      1
      2
      3
      private V unmaskNull(Object value) {
      return (V) (value == NULL ? null : value);
      }

5. EnumMap 的特点

  • 基本用法

    • 如果需要一个 Map 且键是枚举类型,则应该用它
    • 简洁、方便、安全
  • 实现原理

    • 内部有两个数组,长度相同。一个表示所有可能的键;一个表示对应的值,值为 null 表示没有该键值对
    • 键都有一个对应的索引,根据索引可直接访问和操作其键和值,效率很高
-------------------- 本文结束感谢您的阅读 --------------------