EnumMap 类也是 Java 集合框架的成员之一。该类是 Java 集合框架专门为 Map 中键为枚举类型而专门设计实现,它继承自 AbstractMap 并实现了 Map 接口。与普通 Map 相比,EnumMap 利用枚举类型的特性提供了更高的性能和更简洁的实现。
注意,EnumMap 中的所有键都必须来自创建映射时显式或隐式指定的单一枚举类型。EnumMap 在内部表示为数组,这种表示方式极其紧凑且高效。
EnumMap 按照其键的自然顺序(枚举常量声明的顺序)进行维护,这一点体现在集合视图(keySet()、entrySet() 和 values())返回的迭代器中。
注意,keySet ()、entrySet ()、values () 方法返回的集合视图的迭代器具有弱一致性:它们永远不会抛出ConcurrentModificationException 异常,并且可能会(也可能不会)反映迭代过程中对映射所做的任何修改的结果。
EnumMap 不允许键为 null(但是,值允许为 null)。尝试插入 null 键会抛出 NullPointerException 异常。不过,尝试检测 null 键是否存在或移除 null 键的操作可以正常执行。
与大多数集合实现一样,EnumMap 是不同步的。如果多个线程并发访问一个枚举映射,且至少有一个线程修改该映射,则应在外部进行同步。如使用 Collections.synchronizedMap(java.util.Map<K, V>) 方法来“包装”该映射。最好在创建时就这样做,以防止意外的非同步访问:
Map<EnumKey, V> m = Collections.synchronizedMap(new EnumMap<EnumKey, V>(...));
注意,EnumMap 的所有基本操作都以常量时间执行。它们很可能比对应的 HashMap 操作更快。
下面是 EnumMap 的几个重要特性:
(1) 键类型限制:键必须是某个枚举类型的实例,在创建 EnumMap 时必须显式指定枚举类。例如:
EnumMap<Season, String> map = new EnumMap<>(Season.class);
其中,Season 是枚举类。
(2) 存储结构:内部使用数组存储,数组长度等于枚举类的常量个数。利用枚举常量的 ordinal () 值作为数组索引,因此查找、插入、删除操作效率极高(时间复杂度为 O (1))。源码:
/**
* @param <K> 键的类型,必须是枚举类型的子类
* @param <V> 值的类型
*/
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
implements java.io.Serializable, Cloneable {
/**
* 当前映射中所有键所属的枚举类型的Class对象。
* 用于确保所有键都属于同一枚举类型,是构造EnumMap的核心依据。
*/
private final Class<K> keyType;
/**
* 存储当前枚举类型K的所有枚举常量数组。
* 缓存该数组以提升性能,避免频繁调用 Enum.values() 方法。
*/
private transient K[] keyUniverse;
/**
* 该数组是 EnumMap 的核心存储结构。
* 数组索引与 keyUniverse 中枚举常量的顺序一一对应:
* (1)第i个元素表示 keyUniverse[i] 对应的 value 值
* (2)若为 null,可能表示该枚举键未映射任何值,或映射值为 null(通过内部标记区分)
*/
private transient Object[] vals;
/**
* 当前映射中包含的键值对数量。
* 用于快速获取映射大小,避免每次都遍历vals数组计数。
*/
private transient int size = 0;
}这就是 EnumMap 如此高效的原因,这就相当于通过下标操作数组。
(3)有序性:迭代时按照枚举常量在枚举类中的声明顺序排列,无需额外排序操作。源码:
// EnumMap的抽象迭代器实现,用于遍历枚举映射中的元素(键、值或键值对)
private abstract class EnumMapIterator<T> implements Iterator<T> {
/**
* 下一个待返回元素的索引下界(起始查找位置)。
* 用于遍历 vals 数组时记录当前遍历位置,避免重复检查已处理的索引。
*/
int index = 0;
/**
* 最后一个返回元素的索引,若未返回过元素则为-1。
* 用于支持 remove() 方法,确保只能删除最近一次返回的元素。
*/
int lastReturnedIndex = -1;
/**
* 判断迭代器是否还有下一个元素。
* 从当前 index 开始遍历 vals 数组,跳过值为 null 的位置(表示无映射),
* 直到找到有映射的元素或遍历完数组。
*
* @return 若存在下一个元素则返回true,否则返回false
*/
public boolean hasNext() {
// 跳过数组中值为null的位置(无映射),定位到下一个有效元素
while (index < vals.length && vals[index] == null)
index++;
// 若index未超出数组长度,说明存在下一个元素
return index != vals.length;
}
//...
}通过上述源码,hasNext() 方法将从下标为 0 开始遍历数组内容,而 EnumMap 又是通过枚举中常量声明顺序存放的,因此最终迭代的顺序就是枚举声明的顺序。
(4)空值支持:允许值为 null,但键不能为 null。如果键为 null,则抛出 NullPointerException 异常。
(5)非线程安全:EnumMap 本身不是线程安全的,多线程环境下需手动同步,如使用 Collections.synchronizedMap() 方法进行包装。
EnumMap 提供了多种构造方法:
EnumMap(Class<K> keyType) 创建一个指定枚举类型的空 EnumMap。例如:
EnumMap<DayOfWeek, String> map = new EnumMap<>(DayOfWeek.class);
EnumMap(EnumMap<K, ? extends V> m) 复制另一个 EnumMap 的内容(同类型枚举键)。
EnumMap(Map<K, ? extends V> m) 从普通 Map 复制内容,要求该 Map 的键必须是同一枚举类型。
EnumMap 实现了 Map 接口的所有方法,核心方法包括:
向映射中添加键值对。若该键已存在,则覆盖旧值并返回旧值;若不存在,则新增映射并返回 null。注意:
键(key)必须是当前 EnumMap 声明的枚举类型实例,否则抛出 ClassCastException;不允许为 null,否则抛出 NullPointerException。
内部通过枚举常量的 ordinal() 方法获取索引(对应 keyUniverse 数组的位置),直接操作 vals 数组完成赋值,时间复杂度为 O(1)。
若添加的是新键(原位置为 null),会使 size 递增。
批量添加另一个映射(m)中的所有键值对到当前 EnumMap 中。注意:
若 m 是另一个 EnumMap,会直接利用其内部数组高效拷贝,避免重复校验;若为普通 Map,则遍历其键值对逐个调用 put 方法。
若 m 中包含当前 EnumMap 不支持的键类型(非目标枚举类型),会在遍历过程中抛出 ClassCastException。
根据键获取对应的值。若键不存在或值为 null,均返回 null(需通过 containsKey 区分 “键不存在” 和 “值为 null”)。注意:键(key)需是当前枚举类型实例(否则返回 null,不抛异常),通过 ordinal() 索引直接访问 vals 数组,时间复杂度 O(1)。
返回包含所有键的 Set 视图,集合元素按枚举常量的自然顺序(声明顺序)排列。注意:
返回的 Set 是 “视图” 而非副本,修改 EnumMap 会同步反映到该集合中(如新增键会自动加入集合)。
迭代该集合时,使用 EnumMap 内部的弱一致性迭代器,不会抛出 ConcurrentModificationException。
返回包含所有值的 Collection 视图,值的顺序与对应键的枚举自然顺序一致。注意:
与 keySet 类似,是 “视图” 而非副本,支持通过迭代器修改值(但不支持添加 / 删除元素)。
若多个键对应的值为 null,集合中会包含多个 null(与 put 时允许 null 值的特性一致)。
返回包含所有键值对(Map.Entry)的 Set 视图,键值对按键的枚举自然顺序排列。注意:
每个 Entry 实例对应 keyUniverse 和 vals 数组的同一索引,迭代时按顺序遍历有效映射(跳过 vals 中为 null 的位置)。
支持通过 Entry.setValue() 修改值,会同步更新 EnumMap 内部的 vals 数组。
删除指定键对应的键值对,返回被删除的值(若键不存在则返回 null)。注意:
键(key)需是当前枚举类型实例(否则返回 null),通过 ordinal() 索引定位后将 vals 数组对应位置设为 null,并使 size 递减(仅当原位置有值时)。
时间复杂度 O(1),比 HashMap 的删除更高效(无需处理哈希冲突)。
清空映射中所有键值对。注意:内部通过遍历 vals 数组,将所有非 null 位置设为 null,并重置 size 为 0,时间复杂度 O(n)(n 为枚举类型的常量总数)。
判断映射中是否包含指定键。注意:键(key)需是当前枚举类型实例(否则返回 false),通过 ordinal() 索引检查 vals 数组对应位置是否为 null(不为 null 则存在),时间复杂度 O(1)。
判断映射中是否包含指定值。注意:需遍历 vals 数组检查是否有匹配的值(null 值通过 == 判断,非 null 值通过 equals 比较),时间复杂度 O(n)(n 为枚举类型的常量总数)。
返回映射中键值对的数量(非枚举类型的总常量数)。注意:直接返回内部维护的 size 变量,时间复杂度 O(1)(无需遍历数组计数)。
判断映射是否为空(无任何键值对)。注意:通过判断 size == 0 实现,时间复杂度 O(1)。
下面通过一个简单的示例来介绍 EnumMap 的基本用法:
package com.hxstrive.java_collection.enumMap;
import java.util.EnumMap;
import java.util.Map;
// 定义枚举类
enum Season {
SPRING, SUMMER, AUTUMN, WINTER
}
public class EnumMapExample {
public static void main(String[] args) {
// 创建EnumMap
EnumMap<Season, String> seasonDesc = new EnumMap<>(Season.class);
// 添加元素
seasonDesc.put(Season.SPRING, "春暖花开");
seasonDesc.put(Season.SUMMER, "夏日炎炎");
seasonDesc.put(Season.AUTUMN, "秋高气爽");
seasonDesc.put(Season.WINTER, "寒冬腊月");
// 获取元素
System.out.println("春天的描述:" + seasonDesc.get(Season.SPRING));
// 遍历(按枚举声明顺序)
System.out.println("\n所有季节:");
for (Map.Entry<Season, String> entry : seasonDesc.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 检查是否包含键
System.out.println("\n是否包含冬季?" + seasonDesc.containsKey(Season.WINTER));
// 删除元素
seasonDesc.remove(Season.SUMMER);
System.out.println("\n删除夏季后大小:" + seasonDesc.size());
}
}输出结果:
春天的描述:春暖花开 所有季节: SPRING: 春暖花开 SUMMER: 夏日炎炎 AUTUMN: 秋高气爽 WINTER: 寒冬腊月 是否包含冬季?true 删除夏季后大小:3
特性 | EnumMap | HashMap | TreeMap |
键类型 | 仅限枚举类型 | 任意对象 | 任意对象(需可比较) |
性能 | 极高(数组索引访问) | 高(哈希表) | 中(红黑树) |
有序性 | 枚举声明顺序 | 无序 | 自然顺序或比较器顺序 |
内存占用 | 紧凑(固定数组长度) | 较高(哈希表开销) | 较高(树结构开销) |
空键支持 | 不支持 | 支持(仅一个 null 键) | 不支持(自然排序时) |
更多信息参考 https://docs.oracle.com/javase/8/docs/api/java/util/EnumMap.html API 文档。