1. 引言
在 Java 中,有专门用于处理枚举(enum)的集合:EnumSet 和 EnumMap。它们属于标准库(java.util),旨在让对 enum 类型的操作尽可能高效。
EnumSet 和 EnumMap 的内部实现原理?
EnumSet 的工作方式类似位掩码。把它想象成一组开关,每个枚举值正好占用一位。如果某一位被置为 1 — 元素就在集合中;如果为 0 — 则不在。所有位都存储在一个数字数组(long[])中;若你的 enum 少于 64 个值,整个集合只需要一个数就能装下!
EnumMap 更为简单:它是一个值数组,其中索引就是 enum 元素的序号(ordinal)。与常见的 HashMap<Enum, V> 相比,你会得到一个非常紧凑而且快速的结构。
这有什么优势?添加、删除与包含性检查操作都是 O(1)。内存占用极低(尤其是与 HashSet 和 HashMap 相比)。元素的迭代顺序始终是 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 与序列化
EnumSet 和 EnumMap 依赖于你的 enum 的取值集合与声明顺序。如果你新增、删除或重新排序了枚举值,那么已保存或序列化过的集合可能会表现不正确。
序列化
- EnumSet 和 EnumMap 可序列化,但若在序列化与反序列化之间更改了 enum,可能会出现错误或数据丢失。
- 在序列化后从 enum 中删除某个值,几乎必然会在读取时抛出异常。
最佳实践:
- 如果不能保证 enum 稳定,请不要序列化 EnumSet/EnumMap。
- 用于长期持久化时,可改用值的字符串表示列表等方式。
4. 有用的细节
EnumSet/EnumMap 与常规集合对比
| 集合 | 键/元素 | 内部实现 | 性能 | 内存 | Null |
|---|---|---|---|---|---|
|
|
位掩码 | O(1) | 很少 | 不支持 |
|
|
哈希表 | O(1) | 更多 | 支持 |
|
|
按 ordinal 的数组 | O(1) | 很少 | 不支持 |
|
|
哈希表 | O(1) | 更多 | 支持 |
最佳实践
- 当需要值的集合时使用 EnumSet(of、noneOf、allOf、complementOf)。
- 当需要以 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。
如果枚举在运行时被生成或替换(动态加载/反射),这些结构将无法正常工作。
GO TO FULL VERSION