CodeGym /课程 /JAVA 25 SELF /EnumSet/EnumMap

EnumSet/EnumMap

JAVA 25 SELF
第 28 级 , 课程 3
可用

1. 引言

在 Java 中,有专门用于处理枚举(enum)的集合:EnumSetEnumMap。它们属于标准库(java.util),旨在让对 enum 类型的操作尽可能高效。

EnumSet 和 EnumMap 的内部实现原理?

EnumSet 的工作方式类似位掩码。把它想象成一组开关,每个枚举值正好占用一位。如果某一位被置为 1 — 元素就在集合中;如果为 0 — 则不在。所有位都存储在一个数字数组(long[])中;若你的 enum 少于 64 个值,整个集合只需要一个数就能装下!

EnumMap 更为简单:它是一个值数组,其中索引就是 enum 元素的序号(ordinal)。与常见的 HashMap<Enum, V> 相比,你会得到一个非常紧凑而且快速的结构。

这有什么优势?添加、删除与包含性检查操作都是 O(1)。内存占用极低(尤其是与 HashSetHashMap 相比)。元素的迭代顺序始终是 enum 的声明顺序,因此结果具有可预测性。

示例:它长什么样

enum Day { MON, TUE, WED, THU, FRI, SAT, SUN }

EnumSet<Day> weekend = EnumSet.of(Day.SAT, Day.SUN);
System.out.println(weekend); // [SAT, SUN]

从外部看,它就是一个普通集合。但在内部——它是一个数字,其中设置了两个比特位:一个对应 SAT,另一个对应 SUN。若再加入 FRI —— 又会多置一位。不需要哈希表,也不需要额外对象。

EnumMap<Day, String> schedule = new EnumMap<>(Day.class);
schedule.put(Day.MON, "Gym");
schedule.put(Day.FRI, "Party");
System.out.println(schedule); // {MON=Gym, FRI=Party}

这里键(Day)会被转换成内部数组的索引,因此访问的速度与数组下标访问一样快。

2. 使用场景:标志、映射表、有限状态机

EnumSet:非常适合标志与状态集合

  • 标志:为有限数量的选项存储开/关集合。
  • 枚举值集合:如星期几、用户权限、任务状态。
  • 有限状态机(FSM):EnumSet 存储允许的状态迁移十分方便。

示例:访问标志

enum Permission { READ, WRITE, EXECUTE }

EnumSet<Permission> perms = EnumSet.of(Permission.READ, Permission.WRITE);
if (perms.contains(Permission.WRITE)) {
    // 允许写入
}

示例:除去某些值之外的所有值

EnumSet<Day> workdays = EnumSet.complementOf(EnumSet.of(Day.SAT, Day.SUN));
System.out.println(workdays); // [MON, TUE, WED, THU, FRI]

EnumMap:非常适合以枚举为键的映射表

  • 映射表:键是 enum 值,值可以是任意对象。
  • 快速访问:HashMap<Enum, V> 更快且更紧凑。

示例:按星期的价格

EnumMap<Day, Integer> prices = new EnumMap<>(Day.class);
prices.put(Day.MON, 100);
prices.put(Day.SAT, 200);
System.out.println(prices.get(Day.SAT)); // 200

示例:有限状态机

enum State { START, RUNNING, STOPPED }
EnumMap<State, EnumSet<State>> transitions = new EnumMap<>(State.class);
transitions.put(State.START, EnumSet.of(State.RUNNING));
transitions.put(State.RUNNING, EnumSet.of(State.STOPPED));
transitions.put(State.STOPPED, EnumSet.noneOf(State.class));

3. 坑点:修改 enum 与序列化

EnumSetEnumMap 依赖于你的 enum 的取值集合与声明顺序。如果你新增、删除或重新排序了枚举值,那么已保存或序列化过的集合可能会表现不正确。

序列化

  • EnumSetEnumMap 可序列化,但若在序列化与反序列化之间更改了 enum,可能会出现错误或数据丢失。
  • 在序列化后从 enum 中删除某个值,几乎必然会在读取时抛出异常。

最佳实践:

  • 如果不能保证 enum 稳定,请不要序列化 EnumSet/EnumMap
  • 用于长期持久化时,可改用值的字符串表示列表等方式。

4. 有用的细节

EnumSet/EnumMap 与常规集合对比

集合 键/元素 内部实现 性能 内存 Null
EnumSet
enum
位掩码 O(1) 很少 不支持
HashSet<Enum>
enum
哈希表 O(1) 更多 支持
EnumMap
enum
按 ordinal 的数组 O(1) 很少 不支持
HashMap<Enum, V>
enum
哈希表 O(1) 更多 支持

最佳实践

  • 当需要值的集合时使用 EnumSetofnoneOfallOfcomplementOf)。
  • 当需要以 enum 为键的关联表时使用 EnumMap
  • 避免“巨型”枚举——包含数百个值的枚举会降低紧凑性。
  • 如果 enum 可能变化,请不要序列化。
  • 不要使用 null 作为键或值。

5. 实践:如何在应用中使用 EnumSet 和 EnumMap

示例:存储用户角色

enum Role { USER, ADMIN, MODERATOR }

class User {
    private EnumSet<Role> roles = EnumSet.noneOf(Role.class);

    public void addRole(Role role) {
        roles.add(role);
    }

    public boolean isAdmin() {
        return roles.contains(Role.ADMIN);
    }
}

示例:状态迁移表

enum State { NEW, IN_PROGRESS, DONE }

EnumMap<State, EnumSet<State>> transitions = new EnumMap<>(State.class);
transitions.put(State.NEW, EnumSet.of(State.IN_PROGRESS));
transitions.put(State.IN_PROGRESS, EnumSet.of(State.DONE));
transitions.put(State.DONE, EnumSet.noneOf(State.class));

6. 使用 EnumSet/EnumMap 的常见错误

错误 № 1: 将 EnumSet/EnumMap 用于非 enum 类型。
这些集合仅适用于类型为 enum 的情况。

// EnumSet<String> set = EnumSet.of("A", "B"); // 编译错误!

错误 № 2: 在非常大的枚举上使用 EnumSet/EnumMap。
包含数百个值的枚举会降低紧凑性。但即便如此,对于相同数据,它们在内存方面通常仍优于 HashSet/HashMap

错误 № 3: 在序列化之后更改 enum。
在序列化后添加/删除/重新排序枚举值会导致读取时出错或数据丢失。

错误 № 4: 在 EnumSet/EnumMap 中使用 null。
无论是 EnumSet 的元素,还是 EnumMap 的键/值,都不能为 null

EnumSet<Day> days = EnumSet.of(null); // NullPointerException!
EnumMap<Day, String> map = new EnumMap<>(Day.class);
map.put(null, "test"); // NullPointerException!

错误 № 5: 认为 EnumSet 是“普通”的 Set。
EnumSet 只存储其所属 enum 的值;你不能向其中添加不在该枚举中的任意对象。

错误 № 6: 对“可变”的 enum 使用 EnumSet/EnumMap。
如果枚举在运行时被生成或替换(动态加载/反射),这些结构将无法正常工作。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION