EnumMap 和 EnumSet 是什么?为什么它们比 HashMap/HashSet 高效?
简化版
EnumMap 和 EnumSet 是专门为「枚举类型」优化的 Map 和 Set,比用 HashMap/HashSet 存枚举更高效、更省内存。原理是利用「枚举值个数固定、且每个枚举有一个从 0 开始的序号(ordinal())」这个特性:EnumMap 底层就是一个数组——用枚举的 ordinal 作为数组下标直接存 value(不用算哈希、不用处理冲突,O(1) 且缓存友好);EnumSet 底层是一个位向量(bit vector)——用一个 long(64 位)的每一位表示「某个枚举值在不在集合里」(枚举 ≤ 64 个时只用一个 long,极省内存、位运算极快)。所以「用枚举做 key/元素时,优先用 EnumMap/EnumSet」——它们是针对枚举的定制优化,性能远超通用的 HashMap/HashSet。
详细版
EnumMap:用 ordinal 做数组下标:
enum Day { MON, TUE, WED, THU, FRI, SAT, SUN } // ordinal: MON=0, TUE=1, ...
// EnumMap:以枚举为 key
EnumMap<Day, String> schedule = new EnumMap<>(Day.class); // 要传枚举的 Class
schedule.put(Day.MON, "上班");
schedule.put(Day.SAT, "休息");
// 底层:一个数组 Object[],用 key.ordinal() 作下标
// values[Day.MON.ordinal()] = "上班" → values[0] = "上班"
// values[Day.SAT.ordinal()] = "休息" → values[5] = "休息"
// 查找/存储直接用 ordinal 作下标,O(1),无哈希、无冲突
EnumSet:用位向量表示:
// EnumSet:以枚举为元素
EnumSet<Day> workdays = EnumSet.of(Day.MON, Day.TUE, Day.WED, Day.THU, Day.FRI);
EnumSet<Day> weekend = EnumSet.of(Day.SAT, Day.SUN);
EnumSet<Day> all = EnumSet.allOf(Day.class); // 所有枚举值
EnumSet<Day> none = EnumSet.noneOf(Day.class); // 空集
// 底层:一个 long(枚举 ≤ 64 个时),每一位表示一个枚举值在不在
// 工作日 MON-FRI(ordinal 0-4) → 二进制 ...0011111(低 5 位为 1)
// 集合操作就是位运算:并集=|、交集=&、补集=~ → 极快
为什么高效:
| 维度 | HashMap/HashSet | EnumMap/EnumSet |
|---|---|---|
| 底层 | 数组 + 链表/红黑树 | EnumMap=数组、EnumSet=位向量 |
| 定位 | 算 hashCode + 处理冲突 | 直接用 ordinal 作下标 / 位操作 |
| 时间 | 平均 O(1)(有哈希开销) | O(1)(无哈希、无冲突,更快) |
| 内存 | 较大(Node 对象、链表指针) | 极省(数组/一个 long) |
| 顺序 | 无序 | 按 ordinal 顺序(枚举声明顺序) |
⚠️
EnumMap的构造器必须传枚举的Class对象(new EnumMap<>(Day.class))——因为它要在创建时就知道「这个枚举有几个值」,好分配对应大小的数组。这和 HashMap(new HashMap<>()不用传类型)不同。忘了传 Class 会编译不过或抛异常。EnumSet 则用工厂方法创建(EnumSet.of/allOf/noneOf),没有公开构造器。
完整版教学
一、为什么需要枚举专用集合
用枚举做 Map 的 key 或 Set 的元素是很常见的(如「星期→安排」「权限集合」)。用通用的 HashMap/HashSet 当然也能做,但没有利用枚举的特殊性:
枚举的特殊性(EnumMap/EnumSet 能利用的):
① 枚举值的个数是固定的、编译期就知道(Day 就 7 个值)
② 每个枚举值有一个从 0 开始的连续序号 ordinal()(MON=0, TUE=1, ...)
③ 枚举值是单例、可以用 == 比较
用 HashMap 存枚举浪费在哪:
- 要算枚举的 hashCode(多余,枚举有现成的 ordinal)
- 要处理哈希冲突、维护链表(多余,ordinal 天然无冲突)
- 每个 entry 是一个 Node 对象(浪费内存)
EnumMap/EnumSet 的思路是「既然枚举值个数固定、有连续序号,就用数组/位向量直接映射,不用哈希这套重家伙」。这是「针对特定数据类型的定制优化」——通用容器(HashMap)要处理任意类型的 key(所以用哈希),而枚举有更好的性质(连续序号),可以用更简单高效的结构。理解「EnumMap/EnumSet 利用枚举的『个数固定+连续序号』做定制优化」,就理解了它们为什么存在、为什么更高效。
二、EnumMap:数组直接映射
EnumMap 的底层就是「一个数组,用枚举的 ordinal 作下标」——这是它高效的根源:
enum Day { MON, TUE, WED, THU, FRI, SAT, SUN } // ordinal 0~6
EnumMap<Day, String> 底层:
Object[] values = new Object[7]; // 数组大小 = 枚举值个数
put(Day.MON, "上班"): values[Day.MON.ordinal()] = "上班" → values[0] = "上班"
get(Day.MON): return values[Day.MON.ordinal()] → values[0]
对比 HashMap:
HashMap 存枚举:算 hashCode → 定位桶 → 处理冲突 → 找到 Node
EnumMap 存枚举:直接 values[ordinal],一步到位(数组下标访问)
EnumMap 的优势:① 定位是「数组下标访问」(O(1)、极快、无哈希计算、无冲突处理);② 内存省(就一个数组,没有 HashMap 的 Node 对象、链表指针);③ 缓存友好(数组连续内存);④ 有序(按 ordinal 遍历,即枚举声明顺序)。它像「用 ordinal 当下标的定长数组」——因为枚举值个数固定,数组大小刚好、不用扩容。所以「用枚举做 key 就用 EnumMap」——比 HashMap 快、省、有序。理解「EnumMap 用 ordinal 作数组下标直接映射、O(1) 无哈希无冲突」,就掌握了它高效的原理。
三、EnumSet:位向量表示
EnumSet 更巧妙——它用「位向量(bit vector)」表示集合,一个 long 的每一位代表一个枚举值在不在:
enum Day { MON, TUE, WED, THU, FRI, SAT, SUN } // ordinal 0~6
EnumSet<Day> 底层(枚举 ≤ 64 个时用 RegularEnumSet,一个 long):
long elements; // 每一位表示对应 ordinal 的枚举值是否在集合里
EnumSet.of(MON, TUE, WED, THU, FRI): // 工作日 ordinal 0-4
elements = 0b0011111 // 低 5 位为 1,表示 ordinal 0-4 在集合里
集合操作 = 位运算:
add(SAT): elements |= (1L << SAT.ordinal()) // 置位
contains(MON): (elements & (1L << MON.ordinal())) != 0 // 查位
并集 A∪B: A.elements | B.elements
交集 A∩B: A.elements & B.elements
补集: ~elements(在全集范围内)
EnumSet 的优势极其突出:① 内存极省——枚举 ≤ 64 个时,整个集合就是一个 long(8 字节)!(对比 HashSet 每个元素一个 Node 对象);② 操作极快——add/contains/并集/交集全是位运算(一条 CPU 指令级别),比 HashSet 的哈希查找快得多;③ 有序(按 ordinal)。枚举超过 64 个时用 JumboEnumSet(多个 long 数组),思路一样。这种「用位表示集合」是位运算的经典应用(前面「位运算」题讲过位掩码)。所以「用枚举做 Set 元素就用 EnumSet」——内存和速度都碾压 HashSet。理解「EnumSet 用位向量(一个 long)表示集合、操作是位运算、极省极快」,就掌握了它的精妙之处。
四、性能对比:为什么碾压 HashMap/HashSet
把 EnumMap/EnumSet 和 HashMap/HashSet 对比,性能差距一目了然:
存储枚举时的对比:
HashMap/HashSet EnumMap/EnumSet
定位元素 算 hashCode + 找桶 + 冲突 直接 ordinal 下标 / 位操作
时间复杂度 O(1) 但有哈希开销 O(1) 无哈希、更快
单元素内存 一个 Node 对象(约 32 字节) 数组一格 / 一个 bit
整体内存 大(Node + 链表 + 数组) 小(一个数组 / 一个 long)
遍历顺序 无序 有序(ordinal 顺序)
缓存友好 分散(Node 散落堆各处) 好(数组/long 连续)
差距的根源:HashMap/HashSet 是「通用容器」(要处理任意类型 key,所以用哈希这套通用但重的机制),EnumMap/EnumSet 是「专用容器」(利用枚举的连续序号,用数组/位向量这套轻量机制)。就像「通用工具 vs 专用工具」——专用的针对特定场景优化,自然更高效。用一个数字感受:存 5 个枚举,HashSet 要 5 个 Node 对象(约 160 字节)+ 数组,EnumSet 就一个 long(8 字节)——内存差 20 倍。理解「EnumMap/EnumSet 是枚举专用容器、比通用的 HashMap/HashSet 快且省」,就知道了「用枚举优先用它们」的理由。
五、使用方式与注意点
EnumMap/EnumSet 的创建方式和注意点:
// EnumMap:构造器必须传枚举的 Class
EnumMap<Day, String> map = new EnumMap<>(Day.class); // ★ 必须传 Day.class
// EnumSet:用工厂方法(没有公开构造器)
EnumSet<Day> a = EnumSet.of(Day.MON, Day.TUE); // 指定元素
EnumSet<Day> b = EnumSet.allOf(Day.class); // 所有枚举值
EnumSet<Day> c = EnumSet.noneOf(Day.class); // 空集
EnumSet<Day> d = EnumSet.range(Day.MON, Day.FRI); // 范围(ordinal MON~FRI)
EnumSet<Day> e = EnumSet.complementOf(weekend); // 补集
注意点:① EnumMap 必须传 Class(要知道枚举个数以分配数组);② EnumSet 用工厂方法(of/allOf/noneOf/range/complementOf,很丰富);③ 都不允许 null key/元素(枚举 key 不能为 null,会抛 NPE);④ 都是非线程安全(并发要外部同步);⑤ 遍历有序(按 ordinal,即枚举声明顺序——所以别随意调整枚举声明顺序,会影响遍历顺序)。实践建议:只要 key/元素是枚举类型,就用 EnumMap/EnumSet(几乎总是比 HashMap/HashSet 好),这是《Effective Java》推荐的实践。理解「EnumMap 传 Class、EnumSet 用工厂方法、不允许 null、非线程安全、有序」,就能正确使用它们。
六、EnumSet 与位掩码的关系
EnumSet 本质上是「位掩码的类型安全、面向对象的封装」——前面「位运算」题讲过用位掩码(int 的每一位表示一个开关)做权限,EnumSet 是它的升级:
传统位掩码(用 int,不类型安全):
static final int READ = 1, WRITE = 2, EXECUTE = 4;
int perms = READ | WRITE; // 用 | 加、& 查
问题:类型不安全(perms 就是个 int,能传任意 int)、可读性差(数字)
EnumSet(位掩码的现代替代):
enum Permission { READ, WRITE, EXECUTE }
EnumSet<Permission> perms = EnumSet.of(READ, WRITE);
优势:类型安全(只能放 Permission)、可读(枚举名)、API 丰富(add/remove/contains/并交补)
底层:还是位运算(和位掩码一样快),但包装成了类型安全的 Set
《Effective Java》明确建议「用 EnumSet 替代传统的位掩码」——EnumSet 既有位掩码的性能(底层就是位运算),又有类型安全和面向对象的接口(不用手动 |/&、不会传错 int)。所以现代 Java 里「表示一组开关/选项/权限」应该用 EnumSet 而非 int 位掩码。理解「EnumSet 是位掩码的类型安全封装、Effective Java 推荐替代位掩码」,就理解了它的另一层价值——它把「高效的位运算」和「安全的类型系统」结合了。
记忆钩子:「EnumMap/EnumSet 是枚举专用容器,利用枚举『个数固定+连续序号 ordinal』做优化:EnumMap 底层是数组(用 ordinal 作下标直接映射,O(1) 无哈希无冲突)、EnumSet 底层是位向量(一个 long 的每位表示枚举在不在,操作是位运算,极省极快);比 HashMap/HashSet 更快更省内存、有序;EnumMap 构造传 Class、EnumSet 用工厂方法(of/allOf/noneOf);EnumSet 是位掩码的类型安全封装,Effective Java 推荐用枚举做 key/元素时优先用它们」。
七、常见误区与追问
- 误区:EnumMap/EnumSet 和 HashMap/HashSet 差不多。 底层完全不同——EnumMap 用数组(ordinal 下标)、EnumSet 用位向量(一个 long),比 HashMap/HashSet 更快(无哈希无冲突)、更省内存、且有序。
- 误区:EnumMap 用 new EnumMap<>() 就行。 必须传枚举的 Class(
new EnumMap<>(Day.class))——它要在创建时知道枚举个数以分配数组;不传会编译错误。 - 误区:EnumSet 内存和 HashSet 差不多。 EnumSet 枚举 ≤ 64 个时整个集合就是一个 long(8 字节),远比 HashSet(每元素一个 Node 对象)省内存。
- 误区:EnumMap/EnumSet 是无序的。 它们按 ordinal(枚举声明顺序)有序遍历;所以调整枚举声明顺序会影响遍历顺序。
- 追问:EnumMap 为什么比 HashMap 高效? EnumMap 底层是数组、用 key.ordinal() 直接作下标存取(O(1)、无需算哈希、无冲突处理、无 Node 对象),而 HashMap 要算 hashCode、处理冲突、维护 Node,所以 EnumMap 更快更省。
- 追问:EnumSet 底层是怎么实现的? 用位向量——枚举 ≤ 64 个时用一个 long,每一位表示对应 ordinal 的枚举值是否在集合里;add/contains/并集/交集/补集都是位运算(
|/&/~),极快极省。 - 追问:为什么说 EnumSet 是位掩码的替代? 传统位掩码用 int 的每位表示开关(快但类型不安全、可读性差);EnumSet 底层同样是位运算(一样快),但包装成类型安全、可读、API 丰富的 Set,Effective Java 推荐用它替代位掩码。
八、加强记忆
EnumMap 和 EnumSet 是枚举专用的 Map/Set,利用枚举「值个数固定 + 有连续序号 ordinal」的特性做优化,比用 HashMap/HashSet 存枚举更快更省内存。EnumMap 底层是数组——用 key.ordinal() 直接作数组下标存取(O(1)、无需算哈希、无冲突处理、无 Node 对象、缓存友好),构造器必须传枚举 Class(new EnumMap<>(Day.class))。EnumSet 底层是位向量——枚举 ≤ 64 个时整个集合就是一个 long,每一位表示对应枚举值是否在集合里,add/contains/并集/交集/补集全是位运算(极省极快),用工厂方法创建(of/allOf/noneOf/range/complementOf)。相比通用的 HashMap/HashSet(用哈希这套重机制处理任意类型),它们是「专用容器」(用数组/位向量这套轻量机制),性能碾压(内存可差 20 倍)、且有序(按 ordinal)。都不允许 null、非线程安全。EnumSet 是位掩码的类型安全封装(底层位运算一样快,但类型安全可读),《Effective Java》推荐用枚举做 key/元素时优先用 EnumMap/EnumSet、用 EnumSet 替代 int 位掩码。一句话「EnumMap 数组用 ordinal 下标、EnumSet 位向量一个 long,比 HashMap/HashSet 快省有序,EnumMap 传 Class、EnumSet 工厂方法,EnumSet 是位掩码的类型安全替代」。