0%

Java Map 和 Set(八):剖析 EnumSet

1. EnumSet 的概念

  • Set 接口的实现类 HashSet/TreeSet,它们内部都是用对应的 HashMap/TreeMap 实现的,但 EnumSet 不是
  • EnumSet 的实现与 EnumMap 没有任何关系,而是用极为精简和高效的位向量实现的

2. EnumSet 的基本用法

  • HashSet/TreeSet 不同,EnumSet 是一个抽象类,不能直接通过 new 创建

  • EnumSet 提供了若干静态工厂方法,可以创建 EnumSet 类型的对象。比如:public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType);

  • Demo

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //表示星期几的枚举类 Day
    enum Day {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
    }

    //使用 noneOf() 方法
    Set<Day> weekend = EnumSet.noneOf(Day.class);
    weekend.add(Day.SATURDAY);
    weekend.add(Day.SUNDAY);
    System.out.println(weekend);

    //输出
    [SATURDAY, SUNDAY]
  • EnumSet 有很多重载形式的 of() 方法,表示初始集合包括参数中的所有元素。其中有接收可变参数的重载方法,其他重载方法看上去是多余的,之所以有其他重载方法是因为可变参数的运行效率低一些

3. 想象一个场景,在一些工作中(如医生、客服),不是每个工作人员每天都在的,每个人可工作的时间是不一样的。比如张三可能是周一和周三,李四可能是周四和周六。给定每个人可工作的时间,请用 EnumSet 实现下面几个场景需求(预定义代码如下所示)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//表示星期几的枚举类 Day
enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

//表示工作人员的类 Worker
class Worker {
String name;
Set<Day> availableDay;
public Worker(String name, Set<Day> availableDays) {
this.name = name;
this.availableDays = availableDays;
}
//省略 getter 方法
}

//将所有工作人员的信息放到一个数组 workers 中
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))
}

Questions:
有没有哪天一个人都不会来?
有哪些天至少会有一个人来?
有哪些天至少会有两个人来?
有哪些天所有人都会来?
哪些人周一和周二都会来?
  • 有哪些天一个人都不会来?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //实际是在求 worker 时间并集的补集
    Set<Day> days = EnumSet.allOf(Day.class);

    for(Worker w : workers) {
    days.removeAll(w.getAvailableDays());
    }

    System.out.println(days);

    //输出
    [SUNDAY]
  • 有哪些天至少会有一个人来?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //实际是在求 worker 时间的并集
    Set<Day> days = EnumSet.noneOf();

    for(Worker w : workers) {
    days.addAll(w.getAvailableDays());
    }

    System.out.println(days);

    //输出
    [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY]
  • 有哪些天所有人都会来?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //实际是在求 worker 时间的交集
    Set<Day> days = EnumSet.allOf(Day.class);

    for(Worker w : workers) {
    days.retainAll(w.getAvailableDays());
    }

    System.out.println(days);

    //输出
    [TUESDAY]
  • 哪些人周一和周二都会来?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Set<Day> availableWorkers = new HashSet<Worker>();

    for(Worker w : workers) {
    if(w.getAvailableDays().containAll(EnumSet.of(Day.MONDAY, Day.TUESDAY))) {
    availableWorders.add(w);
    }
    }

    for(Worker w : availableWorkers) {
    System.out.println(w.getName());
    }

    //输出
    张三
  • 哪些天至少会有两个人来?

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

    //输出
    [TUESDAY, THURSDAY]

4. EnumSet 的实现原理

  • EnumSet 是使用位向量实现的,表示很简洁、节省空间,大部分操作都是按位运算、效率极高

    • 位向量:用一个位表示一个元素的状态,用一组位表示一个集合的状态,每个位对应一个元素,而状态只可能有两种
    • 位向量能表示的元素个数与向量长度有关,一个 byte 类型能表示 8 个元素,一个 long 类型能表示 64 个元素
  • EnumSet 是一个抽象类,它没有定义使用的向量长度

    • 它有两个子类:RegularEnumSetJumboEnumSet
    • RegularEnumSet 使用一个 long 类型的变量作为位向量,long 类型的位长度是 64
    • JumboEnumSet 使用一个 long 类型的数组
  • 如果枚举值个数小于等于 64,则静态工厂方法中创建的就是 RegularEnumSet;如果大于 64 就是 JumboEnumSet

5. EnumSet 的内部组成

  • EnumSet 也有表示类型信息和所有枚举值的实例变量。如下

    1
    2
    final Class<E> elementType; //表示类型信息
    final Enum[] universe; //表示枚举类的所有枚举值
  • EnumSet 自身没有记录元素个数的变量,也没有位向量,它们是子类维护的

    • 对于 RegularEnumSet,它用一个 long 类型表示位向量。代码为:private long elements = 0L;。它没有定义表示元素个数的变量,是实时计算出来的,计算的代码是:public int size() { return Long.bitCount(elements); }

    • 对于 JumboEnumSet,它用一个 long 数组表示,有单独的 size 变量,代码为

      1
      2
      private long elements[];
      private int size = 0;

6. EnumSet 的特点

  • 对于只有两种状态,且需要进行集合运算的数据,使用位向量进行表示、位运算进行处理。是计算机程序中一种常用的思维方式
  • Java 中有一个更为通用的可动态扩展长度的位向量容器类 BitSet,可以方便地对指定位置的位进行操作,与其他位向量进行位运算
-------------------- 本文结束感谢您的阅读 --------------------