EnumSet 是 Java 集合框架中专为枚举类型设计的 Set 实现,具有极高的性能和紧凑的内存占用。它的元素必须是同一枚举类型的实例,内部通过位向量实现,操作效率极高(如 add、remove、contains 等方法几乎是常量时间)。EnumSet 不允许包含 null 元素,且是有序的,元素顺序与枚举类中声明的顺序一致。它没有公共构造方法,通常通过静态方法(如 allOf()、of()、range() 等)创建实例。
EnumSet 是抽象类,其实际实现有两个,如下图:
其中:
RegularEnumSet 用于枚举常量数量较少(≤64)的情况,使用单个 long 存储
JumboEnumSet 用于枚举常量数量较多(>64)的情况,使用 long 数组存储
注意,这两个实现类会根据枚举类型的大小自动选择,无需手动指定。
allOf(Class<E> elementType) 创建包含指定枚举类型所有元素的 EnumSet
noneOf(Class<E> elementType) 创建指定枚举类型的空 EnumSet
of(E e) 创建包含指定单个枚举元素的 EnumSet
of(E e1, E e2) 创建包含指定两个枚举元素的 EnumSet(可支持最多 5 个参数)
range(E from, E to) 创建包含从 from 到 to 之间所有枚举元素的 EnumSet
copyOf(Collection<E> c) 复制集合 c 中的元素创建 EnumSet
add(E e) 添加元素
addAll(Collection<? extends E> c) 添加集合 c 中的所有元素
remove(E e) 移除元素
contains(E e) 判断是否包含元素
clear() 清空集合
retainAll(Collection<?> c) 仅保留与集合 c 共有的元素
下面通过一个简单例子介绍 EnumSet 的用法:
package com.hxstrive.java_collection.enumSet; import java.util.EnumSet; public class EnumSetDemo { private static enum Day { MONDAY("星期一", 1), TUESDAY("星期二", 2), WEDNESDAY("星期三", 3), THURSDAY("星期四", 4), FRIDAY("星期五", 5), SATURDAY("星期六", 6), SUNDAY("星期日", 7); // 中文名称 private final String chineseName; // 一周中的顺序(1表示周一,7表示周日) private final int order; /** * 构造方法 * @param chineseName 中文名称 * @param order 顺序 */ Day(String chineseName, int order) { this.chineseName = chineseName; this.order = order; } public String getChineseName() { return chineseName; } public int getOrder() { return order; } /** * 判断是否为工作日(周一至周五) * @return 如果是工作日返回true,否则返回false */ public boolean isWeekday() { return this != SATURDAY && this != SUNDAY; } } public static void main(String[] args) { // 1. 创建包含所有星期的EnumSet EnumSet<Day> allDays = EnumSet.allOf(Day.class); System.out.println("所有星期: " + allDays); //所有星期: [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY] // 2. 创建空的EnumSet EnumSet<Day> emptyDays = EnumSet.noneOf(Day.class); System.out.println("空集合: " + emptyDays); //空集合: [] // 3. 创建包含指定单个元素的EnumSet EnumSet<Day> singleDay = EnumSet.of(Day.FRIDAY); System.out.println("只包含周五: " + singleDay); //只包含周五: [FRIDAY] // 4. 创建包含多个指定元素的EnumSet EnumSet<Day> someDays = EnumSet.of(Day.MONDAY, Day.WEDNESDAY, Day.FRIDAY); System.out.println("包含周一、周三、周五: " + someDays); //包含周一、周三、周五: [MONDAY, WEDNESDAY, FRIDAY] // 5. 创建包含一定范围元素的EnumSet(从周一到周五) EnumSet<Day> workDays = EnumSet.range(Day.MONDAY, Day.FRIDAY); System.out.println("工作日(周一到周五): " + workDays); //工作日(周一到周五): [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY] // 6. 创建包含周末的EnumSet(通过复制已有集合的补集) EnumSet<Day> weekend = EnumSet.complementOf(workDays); System.out.println("周末(周六和周日): " + weekend); //周末(周六和周日): [SATURDAY, SUNDAY] // 7. 集合操作:添加元素 EnumSet<Day> holidays = EnumSet.copyOf(weekend); holidays.add(Day.FRIDAY); // 假设周五也放假 System.out.println("假期(周末+周五): " + holidays); //假期(周末+周五): [FRIDAY, SATURDAY, SUNDAY] // 8. 集合操作:移除元素 holidays.remove(Day.FRIDAY); System.out.println("移除周五后的假期: " + holidays); //移除周五后的假期: [SATURDAY, SUNDAY] // 9. 集合操作:判断包含关系 boolean hasSaturday = weekend.contains(Day.SATURDAY); System.out.println("周末包含周六吗?" + hasSaturday); //周末包含周六吗?true // 10. 集合操作:求交集 EnumSet<Day> intersection = EnumSet.copyOf(workDays); intersection.retainAll(someDays); // 与周一、周三、周五的交集 System.out.println("工作日与{周一,周三,周五}的交集: " + intersection); //工作日与{周一,周三,周五}的交集: [MONDAY, WEDNESDAY, FRIDAY] // 11. 遍历EnumSet System.out.println("\n遍历所有星期:"); for (Day day : allDays) { System.out.println(day.getOrder() + ": " + day + (day.isWeekday() ? " (工作日)" : " (周末)")); } //遍历所有星期: //1: MONDAY (工作日) //2: TUESDAY (工作日) //3: WEDNESDAY (工作日) //4: THURSDAY (工作日) //5: FRIDAY (工作日) //6: SATURDAY (周末) //7: SUNDAY (周末) } }
以下是 EnumSet 的部分源码,查看成员变量定义和构造方法定义:
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E> implements Cloneable, java.io.Serializable { // 枚举的类型信息 final Class<E> elementType; // 枚举常量数组(缓存,避免多次调用Enum.values()) final Enum<?>[] universe; // 私有构造方法,供子类调用 EnumSet(Class<E> elementType, Enum<?>[] universe) { this.elementType = elementType; this.universe = universe; } // ... 其他方法 }
注意,EnumSet 本身是抽象类,实际上通过两个子类实现,如下:
RegularEnumSet 适用于枚举常量数量 ≤ 64 的情况,用 long 存储位向量。
JumboEnumSet 适用于枚举常量数量 > 64 的情况,用 long[] 存储位向量。
通过 noneOf(Class<E> elementType) 方法可以创建指定枚举类型的空 EnumSet,方法定义如下:
/** * 创建一个指定枚举类型的空 EnumSet * 该方法是 EnumSet 的核心静态工厂方法之一,负责根据枚举常量数量自动选择最优实现类。 * * @param <E> 集合中元素的类型,必须是 Enum 的子类(即枚举类型) * @param elementType 目标 EnumSet 要存储的枚举类型的 Class 对象 * @return 一个指定枚举类型的空 EnumSet 实例(具体是 RegularEnumSet 或 JumboEnumSet) * @throws NullPointerException 如果传入的 elementType 为 null(无法确定枚举类型) */ public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) { // 1. 获取指定枚举类型的所有常量数组 // 该数组是 JDK 内部缓存的,避免每次调用 Enum.values() 创建新数组,提升性能 Enum<?>[] universe = getUniverse(elementType); // 2. 校验 elementType 是否为合法枚举类型 // 若 universe 为 null,说明 elementType 不是枚举类,抛出类型转换异常 if (universe == null) throw new ClassCastException(elementType + " not an enum"); // 3. 根据枚举常量数量选择对应的 EnumSet 实现类 // 当常量数量 <= 64 时,使用 RegularEnumSet(基于单个 long 位向量存储,更高效) // 当常量数量 > 64 时,使用 JumboEnumSet(基于 long 数组存储,支持更多元素) if (universe.length <= 64) return new RegularEnumSet<>(elementType, universe); else return new JumboEnumSet<>(elementType, universe); } /** * 获取指定枚举类型 E 的所有常量组成的数组 * * @param <E> 目标枚举类型,必须是 Enum 的子类 * @param elementType 要获取常量数组的枚举类型的 Class 对象 * @return 该枚举类型所有常量组成的数组(缓存的共享实例) * 若 elementType 不是枚举类型,返回 null */ private static <E extends Enum<E>> E[] getUniverse(Class<E> elementType) { // 1. 通过 SharedSecrets 获取 Java 语言内部访问器(JavaLangAccess) // SharedSecrets 是 JDK 内部机制,用于在不暴露 public API 的情况下, // 让核心类(如 EnumSet)访问JDK内部功能 // // 2. 调用 JavaLangAccess 的 getEnumConstantsShared 方法,获取枚举类型的缓存常量数组 // 该方法返回的数组是枚举类初始化时创建并缓存的单例数组,所有调用者共享同一实例, // 避免重复克隆数组的性能消耗 return SharedSecrets.getJavaLangAccess().getEnumConstantsShared(elementType); }
通过 allOf(Class<E> elementType) 方法,创建枚举类型 elementType 所有常量的 EnumSet 实例,相当于调用枚举的 values() 方法,将返回的枚举常量数组存入 EnumSet:
/** * 创建一个包含指定枚举类型所有常量的 EnumSet * 即集合内容与该枚举类型的完整常量列表完全一致(枚举有多少个常量,集合就包含多少个元素) * * @param <E> 集合中元素的类型,必须是 Enum 的子类(枚举类型) * @param elementType 目标 EnumSet 要包含的枚举类型的 Class 对象 * @return 包含该枚举类型所有常量的 EnumSet 实例 * @throws NullPointerException 如果传入的 elementType 为 null(无法确定枚举类型及常量列表) */ public static <E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType) { // 1. 先通过 noneOf() 创建指定枚举类型的空 EnumSet // 注意:noneOf() 会自动根据枚举常量数量选择 RegularEnumSet 或 JumboEnumSet // 确保底层存储适配 EnumSet<E> result = noneOf(elementType); // 2. 调用 EnumSet 的抽象方法 addAll(),向空集合中添加该枚举类型的所有常量 // 注意:addAll() 的具体实现由子类(RegularEnumSet 或 JumboEnumSet)提供 // RegularEnumSet 通过位运算将所有位设为 1,批量添加所有常量 // JumboEnumSet 通过遍历 long 数组,将所有数组元素的位设为 1,批量添加所有常量 result.addAll(); return result; }
通过 of(E e) 方法以及重载的 of() 方法,根据传递的初始值创建 EnumSet,重载方法签名如下:
源码定义如下:
/** * 创建一个初始包含指定单个枚举元素的 EnumSet * * 注:本方法是一系列重载方法之一,分别支持初始化包含 1 到 5 个元素的 EnumSet。 * 第六个重载版本使用可变参数(varargs)特性,可创建包含任意数量元素的 EnumSet, * 但相比非可变参数的重载版本,其运行速度可能较慢(因可变参数存在数组创建开销)。 * * @param <E> 指定元素的类型及集合的元素类型(必须是枚举类型) * @param e 集合初始要包含的单个元素 * @throws NullPointerException 如果传入的元素 e 为 null(EnumSet 不允许 null 元素) * @return 初始包含指定元素的 EnumSet 实例 */ public static <E extends Enum<E>> EnumSet<E> of(E e) { // 1. 通过元素 e 的枚举类型(e.getDeclaringClass())创建空EnumSet // getDeclaringClass() 返回该枚举常量所属的枚举类 Class 对象,确保集合类型与元素类型一致 EnumSet<E> result = noneOf(e.getDeclaringClass()); // 2. 向空集合中添加指定元素e // add() 方法由具体子类(RegularEnumSet/JumboEnumSet)实现,通过位运算标记元素存在 result.add(e); return result; }
通过 range(E from, E to) 方法创建一个初始元素包含指定两个端点(from~to)所定义范围内所有元素的EnumSet。
注意,返回的 EnumSet 会包含两个端点元素本身,若两个端点相同(from == to),则集合只包含该单个元素。但两个端点必须符合顺序要求(from 不能在枚举定义中位于 to 之后)。
源码定义如下:
/** * 创建一个初始包含指定两个端点所定义范围内所有元素的 EnumSet * * @param <E> 参数元素及集合的元素类型(必须是枚举类型) * @param from 范围的起始端点元素(包含在集合中) * @param to 范围的结束端点元素(包含在集合中) * @throws NullPointerException 如果 from 或 to 为 null(EnumSet 不允许 null元素) * @throws IllegalArgumentException 如果 from 在枚举顺序中位于 to 之后(from.compareTo(to) > 0) * @return 包含 from 到 to(含两端点)所有枚举元素的 EnumSet 实例 */ public static <E extends Enum<E>> EnumSet<E> range(E from, E to) { // 1. 校验范围的合法性:确保from不位于to之后 // 通过枚举的 compareTo 方法比较顺序(基于枚举常量的定义顺序) if (from.compareTo(to) > 0) throw new IllegalArgumentException(from + " > " + to); // 2. 根据 from 所属的枚举类型创建空 EnumSet EnumSet<E> result = noneOf(from.getDeclaringClass()); // 3. 调用抽象方法 addRange(),向空集合中添加 from 到 to 范围内的所有元素 // 具体实现由子类(RegularEnumSet 或 JumboEnumSet)提供: // (1)利用枚举常量的 ordinal() 值连续性,通过位运算批量添加范围内元素 // (2)比逐个调用add()方法更高效,尤其对于大范围元素场景 result.addRange(from, to); return result; }
上面介绍了如何通过 EnumSet 的 noneOf()、allOf()、of()、range() 方法去创建实例,但是将枚举元素添加到 EnumSet 均是通过抽象方法 addRange()、add()、addAll() 完成,由具体子类去实现,下面将介绍 RegularEnumSet 和 JumboEnumSet 具体实现类。
RegularEnumSet 是 Java 集合框架中 EnumSet 的一个私有实现类,专门用于处理元素数量不超过 64 个的枚举类型("常规大小" 的枚举)。它通过位运算实现高效的集合操作,是枚举集合的核心实现之一。
类定义如下:
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> { @java.io.Serial private static final long serialVersionUID = 3411599620347842686L; /** * 此集合的位向量表示。第 2^k 位表示 universe[k] 存在于此集合中。 * 使用 long 类型的位运算来高效存储枚举元素,每个位对应一个枚举常量 * 例如,bit 0 对应枚举中的第一个元素,bit 1 对应第二个元素,以此类推 */ private long elements = 0L; /** * 创建一个指定枚举类型和枚举常量数组的 RegularEnumSet * @param elementType 枚举类型的 Class 对象 * @param universe 包含所有枚举常量的数组(枚举的完整值集合) */ RegularEnumSet(Class<E> elementType, Enum<?>[] universe) { super(elementType, universe); } /** * 添加从 from 到 to(包括两者)的所有枚举元素到集合中 * 使用位运算高效设置范围内的所有位。 * * @param from 起始枚举元素(包含) * @param to 结束枚举元素(包含) */ void addRange(E from, E to) { // 位运算逻辑解析: // 1. (from.ordinal() - to.ordinal() - 1) 计算需要移位的位数 // 2. -1L >>> [位数] 生成从0到指定位置的连续1的位掩码 // 3. << from.ordinal() 将生成的位掩码左移到起始位置 // 最终结果:from 到 to 之间的所有位都被设置为1 // 例如:将 2 ~ 6 之间的位设置为1,以 int 为例 // -1 => 1111 1111 1111 1111 1111 1111 1111 1111 // -1 >>> (2 - 6 -1) >>> -5 >>> -5%32 >>> 27 // -1 >>> 27 => 0000 0000 0000 0000 0000 0000 0001 1111 // << 2 => 0000 0000 0000 0000 0000 0000 0111 1100 elements = (-1L >>> (from.ordinal() - to.ordinal() - 1)) << from.ordinal(); } /** * 将所有可能的枚举元素添加到集合中(即包含枚举类型的所有常量) */ void addAll() { // 如果枚举常量数组不为空 if (universe.length != 0) // 生成一个前 universe.length 位都为 1 的位掩码 // 当 universe.length <= 64 时,-1L >>> -universe.length // 等价于 // (1L << universe.length) - 1 elements = -1L >>> -universe.length; } //...省略... }
注意:
(1)ordinal() 是 Enum 类的一个内置方法,用于返回枚举常量在其枚举声明中的位置索引(从 0 开始)。
(2)在 Java 中,>>> 是无符号右移运算符,它的作用是将一个整数的二进制位向右移动指定的位数,且高位始终用 0 填充(无论原数是正数还是负数)。
当 >>> 移动的位数是负数时,Java 会对这个负数进行特殊处理:将移动位数对 32(对于 int 类型)或 64(对于 long 类型)取模,最终使用模运算的结果作为实际移动位数。
具体规则:
对于 int 类型(32 位)若移动位数为 -n,实际移动位数为 -n % 32。例如:a >>> -1 等价于 a >>> 31(因为 -1 % 32 = 31)。
对于 long 类型(64 位)若移动位数为 -n,实际移动位数为 -n % 64。例如:b >>> -2 等价于 b >>> 62(因为 -2 % 64 = 62)。
从集合删除元素的源码如下:
/** * 从集合中移除指定的元素(如果该元素存在) * * @param e 要从集合中移除的元素(如果存在) * @return 如果集合中包含该元素并成功移除,则返回 true;否则返回 false */ public boolean remove(Object e) { // 枚举集合中不允许存在null元素,直接返回false if (e == null) return false; // 获取元素的类对象,用于类型检查 Class<?> eClass = e.getClass(); // 检查元素类型是否与当前集合的枚举类型一致 // 若元素的类不是当前枚举类型,且其超类也不是当前枚举类型,则不是该集合中的元素 if (eClass != elementType && eClass.getSuperclass() != elementType) return false; // 保存当前的元素位向量,用于后续判断是否发生变化 long oldElements = elements; // 通过位运算移除元素,核心逻辑: // 1. ((Enum<?>)e).ordinal() 获取该枚举元素的序号 // 2. 1L << 序号:生成只有该元素对应位为1的位掩码 // 3. ~运算:对上述位掩码取反,得到只有该元素对应位为0、其他位为1的掩码 // 4. elements & 上述结果:将 elements 中该元素对应的位设为 0(即移除该元素) elements &= ~(1L << ((Enum<?>)e).ordinal()); // 若移除前后的位向量不同,说明成功移除了元素,返回 true;否则返回 false return elements != oldElements; }
到这里,RegularEnumSet 中新增和删除的方法就介绍完了,更多方法实现读者自行阅读。
JumboEnumSet 是 Java 集合框架中 EnumSet 的另一个私有实现类,专门用于处理元素数量超过 64 个的枚举类型("大型" 枚举)。它与 RegularEnumSet 共同构成了枚举集合的完整实现,根据枚举常量的数量自动选择合适的实现类。
当枚举类型包含的常量数量 超过 64 个 时,EnumSet 的静态工厂方法(如 EnumSet.of()、EnumSet.allOf())会自动返回 JumboEnumSet 实例。这是因为 RegularEnumSet 基于 long 类型的位向量(仅能表示 64 位),无法处理更多元素。
JumboEnumSet 内部使用 long[] 数组作为位向量存储元素,数组中的每个 long 元素可表示 64 个枚举常量:
数组索引 i 对应的 long 值,负责存储序号范围为 [i×64, (i+1)×64 - 1] 的枚举常量。
若第 k 位为 1,表示集合包含对应序号的枚举元素。
类定义如下:
class JumboEnumSet<E extends Enum<E>> extends EnumSet<E> { @java.io.Serial private static final long serialVersionUID = 334349849919042784L; /** * 此集合的位向量表示。数组中第j个元素的第i位表示 * universe[64*j + i]是否存在于该集合中 * 说明:使用 long 数组分段存储位向量,每个 long 处理64个枚举元素 */ private long elements[]; /** * 集合中元素的数量 */ private int size = 0; /** * 创建指定枚举类型和枚举常量数组的 JumboEnumSet * * @param elementType 枚举类型的Class对象 * @param universe 包含所有枚举常量的完整数组 */ JumboEnumSet(Class<E> elementType, Enum<?>[] universe) { super(elementType, universe); // 计算存储所有枚举元素所需的long数组长度 // (universe.length + 63) >>> 6 等价于 (universe.length + 63) / 64 // 确保数组长度能容纳所有枚举元素 elements = new long[(universe.length + 63) >>> 6]; } /** * 添加从 from 到 to(包括两者)的所有枚举元素到集合中 * * @param from 起始枚举元素(包含) * @param to 结束枚举元素(包含) */ void addRange(E from, E to) { // 计算起始元素在long数组中的索引(ordinal / 64) int fromIndex = from.ordinal() >>> 6; // 计算结束元素在long数组中的索引(ordinal / 64) int toIndex = to.ordinal() >>> 6; // 如果起始和结束元素在同一个long数组元素中 if (fromIndex == toIndex) { // 生成从from到to的连续位掩码并设置 elements[fromIndex] = (-1L >>> (from.ordinal() - to.ordinal() - 1)) << from.ordinal(); } else { // 处理起始元素所在的long:从from开始到该long末尾的所有位设为1 elements[fromIndex] = (-1L << from.ordinal()); // 处理中间的所有long:全部设为1(表示所有64位都有元素) for (int i = fromIndex + 1; i < toIndex; i++) elements[i] = -1; // 处理结束元素所在的long:从开始到to的所有位设为1 elements[toIndex] = -1L >>> (63 - to.ordinal()); } // 更新集合大小 size = to.ordinal() - from.ordinal() + 1; } /** * 将所有可能的枚举元素添加到集合中(即包含枚举类型的所有常量) */ void addAll() { // 先将所有long元素都设为-1(二进制全为1) for (int i = 0; i < elements.length; i++) elements[i] = -1; // 对最后一个long元素进行调整,只保留有效位数(枚举元素实际数量可能不是64的整数倍) elements[elements.length - 1] >>>= -universe.length; // 更新集合大小为枚举元素总数 size = universe.length; } //...省略... }
从集合删除元素的源码如下:
/** * 从集合中移除指定的元素(如果该元素存在) * * @param e 要从集合中移除的元素(如果存在) * @return 如果集合中包含该元素并成功移除,则返回{@code true};否则返回{@code false} */ public boolean remove(Object e) { // 枚举集合中不允许存在null元素,直接返回false if (e == null) return false; // 获取元素的类对象,用于类型检查 Class<?> eClass = e.getClass(); // 检查元素类型是否与当前集合的枚举类型一致 // 若元素的类不是当前枚举类型,且其超类也不是当前枚举类型,则不是该集合中的元素 if (eClass != elementType && eClass.getSuperclass() != elementType) return false; // 获取元素在枚举中的序号 int eOrdinal = ((Enum<?>)e).ordinal(); // 计算该元素在long数组中的索引(ordinal / 64) int eWordNum = eOrdinal >>> 6; // 保存当前的long值,用于后续判断是否发生变化 long oldElements = elements[eWordNum]; // 核心逻辑: // 1. 1L << eOrdinal:生成只有该元素对应位为 1 的位掩码 // 2. ~运算:对上述位掩码取反 // 3. elements[eWordNum] & 上述结果:将该元素对应的位设为0(即移除该元素) elements[eWordNum] &= ~(1L << eOrdinal); // 判断是否成功移除元素 boolean result = (elements[eWordNum] != oldElements); // 如果成功移除,更新集合大小 if (result) size--; return result; }
更多信息请参考 https://docs.oracle.com/javase/8/docs/api/java/util/EnumSet.html API 文档。