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 的子类。LinkedHashMapHashMapLinkedList 的结合体:它维护了指向前后条目的指针,因此能保持条目的插入顺序(普通 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));
}

四个测试用例分别验证:

  1. ✅ 无限制模式:即使添加大量条目也不抛异常
  2. ✅ 未达限制:添加条目数小于限制时不抛异常
  3. ✅ 达到限制时更新值:替换已存在键的值不抛异常
  4. ✅ 超出限制:尝试添加新条目时抛出带特定信息的 RuntimeException

6. 总结

在某些特定场景下,可能需要强制限制 HashMap 的最大大小。本文提供了两种实现方案:

  • 扩展 LinkedHashMap 并重写 removeEldestEntry():适合快速实现LRU缓存等场景
  • 扩展 HashMap 并重写插入方法:提供更严格的大小限制

选择方案时需权衡需求:前者允许自动移除旧条目,后者则完全阻止超出限制的插入。实际应用中,根据具体场景选择最合适的实现即可。

示例代码可在 GitHub 获取。


原始标题:Limiting the Max Size of a HashMap in Java