1. 概述
HashMap
是Java集合库中广为人知的类,它实现了 Map
接口,允许存储键值对。**HashMap
实例对条目数量没有限制**。但在某些特定场景下,我们可能需要改变这一行为。本文将探讨几种强制限制 HashMap
大小的方法。
2. Java HashMap 的核心概念
HashMap
的本质是一个哈希表。哈希表是基于两种基础数据结构构建的:数组和链表。
2.1. 内部结构
数组是 HashMap
的基本存储单元。数组的每个位置都包含一个链表引用。链表可以存储一组由键和值组成的条目。键和值都是Java对象(非基本类型),且键是唯一的。HashMap
接口定义了 put
方法:
V put(K key, V value)
该方法使用所谓的“哈希函数”,根据输入键计算出一个称为“哈希值”的数字。然后基于哈希值和当前数组大小,计算出插入条目的索引位置。
不同键可能产生相同哈希值,进而导致相同索引,这会产生冲突。当冲突发生时,新条目会被插入到该索引位置的链表末尾。
如果链表中的条目数量超过特定阈值(由 TREEIFY_THRESHOLD
常量定义),HashMap
会将链表替换为树结构,将时间复杂度从 O(n) 优化到 *O(log n)*。
2.2. 扩容、重哈希与加载因子
从性能角度看,最理想的情况是条目均匀分布在整个数组中,每个位置最多只有一个条目。但随着条目数量增加,冲突频率上升,链表长度也随之增长。
为了尽可能保持理想状态,当条目数量达到特定阈值时,HashMap
会自动扩容,并重新计算所有条目的哈希值和索引位置。
HashMap
的扩容基于“加载因子”。加载因子定义为:条目数量与可用位置数的比值达到上限时触发扩容和重哈希。HashMap
的默认加载因子是 0.75f
。
3. 需要限制HashMap大小的场景
HashMap
的大小仅受JVM可用内存限制。这是该类的设计初衷,也符合哈希表的常规使用场景。
但在某些特定情况下,我们可能需要施加自定义限制,例如:
- ✅ 实现缓存机制
- ✅ 在可控条件下收集
HashMap
的性能指标,避免自动扩容阶段干扰
如后续章节所述,第一种场景可以使用 LinkedHashMap
及其 removeEldestEntry
方法实现;第二种场景则需要自定义 HashMap
扩展。
这些场景与 HashMap
的原始设计目标相去甚远:缓存是比简单映射更复杂的概念,而扩展原始类则使其无法作为纯黑盒进行测试。
4. 使用LinkedHashMap限制最大大小
限制最大大小的一种方法是使用 LinkedHashMap
——HashMap
的子类。LinkedHashMap
是 HashMap
和 LinkedList
的结合体:它维护了指向前后条目的指针,因此能保持条目的插入顺序(普通 HashMap
不支持此特性)。
4.1. LinkedHashMap 示例
通过重写 removeEldestEntry()
方法可实现大小限制。该方法返回布尔值,在插入新条目后由内部调用,用于判断是否应移除最旧的条目。
“最旧条目”取决于 accessOrder
参数:
-
false
(默认值):最早插入的键 -
true
:最近最少访问的键
重写 removeEldestEntry()
可定义自定义规则:
@Test
void givenLinkedHashMapObject_whenAddingNewEntry_thenEldestEntryIsRemoved() {
final int MAX_SIZE = 4;
LinkedHashMap<Integer, String> linkedHashMap;
linkedHashMap = new LinkedHashMap<Integer, String>() {
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, String> eldest) {
return size() > MAX_SIZE;
}
};
linkedHashMap.put(1, "One");
linkedHashMap.put(2, "Two");
linkedHashMap.put(3, "Three");
linkedHashMap.put(4, "Four");
linkedHashMap.put(5, "Five");
String[] expectedArrayAfterFive = { "Two", "Three", "Four", "Five" };
assertArrayEquals(expectedArrayAfterFive, linkedHashMap.values()
.toArray());
linkedHashMap.put(6, "Six");
String[] expectedArrayAfterSix = { "Three", "Four", "Five", "Six" };
assertArrayEquals(expectedArrayAfterSix, linkedHashMap.values()
.toArray());
}
上述示例中,我们创建了 LinkedHashMap
实例并重写了 removeEldestEntry
方法。当插入新条目导致大小超过限制时,实例会自动移除最旧的条目。
测试用例中预先设置最大大小为4,填入4个条目后,每次新增条目时:
- 总条目数始终保持在4
-
LinkedHashMap
每次都会移除最旧的条目
⚠️ 注意:这并非严格限制大小——插入操作仍被允许,只是通过移除旧条目维持大小上限。
通过将 accessOrder
设为 true
,可实现LRU(最近最少使用)缓存。
4.2. 性能考量
普通 HashMap
具备哈希表应有的性能。而 LinkedHashMap
需维护条目顺序,使其插入和删除操作变慢。访问操作性能不变,除非设置 accessOrder=true
。
5. 使用自定义HashMap限制最大大小
另一种策略是扩展 HashMap
并自定义 put
方法实现,当达到限制时抛出异常。
5.1. 实现自定义HashMap
需重写 put
方法,添加前置检查:
public class HashMapWithMaxSizeLimit<K, V> extends HashMap<K, V> {
private int maxSize = -1;
public HashMapWithMaxSizeLimit(int maxSize) {
super();
this.maxSize = maxSize;
}
@Override
public V put(K key, V value) {
if (this.maxSize == -1 || this.containsKey(key) || this.size() < this.maxSize) {
return super.put(key, value);
}
throw new RuntimeException("Max size exceeded!");
}
}
上述 HashMap
扩展类中:
-
maxSize
属性默认值为-1
,表示“无限制” - 通过构造函数可修改该属性
-
put
实现中,若满足以下任一条件,调用父类方法: -
maxSize
为默认值(无限制) - 键已存在(更新操作)
- 当前条目数小于
maxSize
- 否则抛出非受检异常
❌ 注意:此处限制可被 putAll
绕过。为避免此问题,还需重写 putAll
方法。
为简化示例,未重写父类所有构造函数。实际使用时应重写所有构造函数以保持 HashMap
原始设计,尤其需在 HashMap(Map<? extends K, ? extends V> m)
构造函数中应用大小限制逻辑(因其直接添加条目而不调用 putAll
)。
此方案比前一种更严格:**HashMap
会阻止超出限制的插入操作,且不删除任何现有元素**。
5.2. 测试自定义HashMap
测试上述自定义类:
private final int MAX_SIZE = 4;
private HashMapWithMaxSizeLimit<Integer, String> hashMapWithMaxSizeLimit;
@Test
void givenCustomHashMapObject_whenThereIsNoLimit_thenDoesNotThrowException() {
hashMapWithMaxSizeLimit = new HashMapWithMaxSizeLimit<Integer, String>();
assertDoesNotThrow(() -> {
for (int i = 0; i < 10000; i++) {
hashMapWithMaxSizeLimit.put(i, i + "");
}
});
}
@Test
void givenCustomHashMapObject_whenLimitNotReached_thenDoesNotThrowException() {
hashMapWithMaxSizeLimit = new HashMapWithMaxSizeLimit<Integer, String>(MAX_SIZE);
assertDoesNotThrow(() -> {
for (int i = 0; i < 4; i++) {
hashMapWithMaxSizeLimit.put(i, i + "");
}
});
}
@Test
void givenCustomHashMapObject_whenReplacingValueWhenLimitIsReached_thenDoesNotThrowException() {
hashMapWithMaxSizeLimit = new HashMapWithMaxSizeLimit<Integer, String>(MAX_SIZE);
assertDoesNotThrow(() -> {
hashMapWithMaxSizeLimit.put(1, "One");
hashMapWithMaxSizeLimit.put(2, "Two");
hashMapWithMaxSizeLimit.put(3, "Three");
hashMapWithMaxSizeLimit.put(4, "Four");
hashMapWithMaxSizeLimit.put(4, "4");
});
}
@Test
void givenCustomHashMapObject_whenLimitExceeded_thenThrowsException() {
hashMapWithMaxSizeLimit = new HashMapWithMaxSizeLimit<Integer, String>(MAX_SIZE);
Exception exception = assertThrows(RuntimeException.class, () -> {
for (int i = 0; i < 5; i++) {
hashMapWithMaxSizeLimit.put(i, i + "");
}
});
String messageThrownWhenSizeExceedsLimit = "Max size exceeded!";
String actualMessage = exception.getMessage();
assertTrue(actualMessage.equals(messageThrownWhenSizeExceedsLimit));
}
四个测试用例分别验证:
- ✅ 无限制模式:即使添加大量条目也不抛异常
- ✅ 未达限制:添加条目数小于限制时不抛异常
- ✅ 达到限制时更新值:替换已存在键的值不抛异常
- ✅ 超出限制:尝试添加新条目时抛出带特定信息的
RuntimeException
6. 总结
在某些特定场景下,可能需要强制限制 HashMap
的最大大小。本文提供了两种实现方案:
- 扩展
LinkedHashMap
并重写removeEldestEntry()
:适合快速实现LRU缓存等场景 - 扩展
HashMap
并重写插入方法:提供更严格的大小限制
选择方案时需权衡需求:前者允许自动移除旧条目,后者则完全阻止超出限制的插入。实际应用中,根据具体场景选择最合适的实现即可。
示例代码可在 GitHub 获取。