Skip to content

HashMap in Java

在 Java 的集合框架中,HashMap 是最常用的键值对存储容器之一。它以 哈希表(Hash Table) 作为底层数据结构,通过哈希算法实现了接近常数时间复杂度的查找和插入操作。

本文将重点介绍 Java 8 之前(主要是 Java 7 及以下版本)HashMap 的底层实现原理,包括其 哈希计算、扩容机制、冲突处理策略 等内容,并对比 Java 8 之后的改进(红黑树结构)。


基础使用层的内容

API使用方式(CRUD)

  • Create 增加元素 将一个键值对(元素)添加到 HashMap 中,可以使用 put() 方法。例如,将名字和年龄作为键值对添加到 HashMap 中:
java
final var map = new HashMap<String, Integer>();
map.put("张三", 20);
map.put("李四", 25);
  • Delete 删除元素 从 HashMap 中删除一个键值对,可以使用 remove() 方法。例如,删除名字为 "张三" 的键值对:
java
map.remove("张三");
  • Update 修改元素 修改 HashMap 中的一个键值对,可以使用 put() 方法。例如,将名字为 "张三" 的年龄修改为 30:
java
map.put("张三", 30);
  • Read 查询元素 从 HashMap 中查找一个键对应的值,可以使用 get() 方法。例如,查找名字为 "张三" 的年龄:
java
final var age = map.get("张三");

在实际应用中,HashMap 可以用于缓存、索引等场景。例如,可以将用户 ID 作为键,用户信息作为值,将用户信息缓存到 HashMap 中,以便快速查找。又如,可以将关键字作为键,文档 ID 列表作为值,将文档索引缓存到 HashMap 中,以便快速搜索文档。

HashMap 的实现原理是基于哈希表的,它的底层是一个数组,数组的每个位置可能是一个链表或红黑树,也可能只是一个键值对(后面会讲)。当添加一个键值对时,HashMap 会根据键的哈希值计算出该键对应的数组下标(索引),然后将键值对插入到对应的位置。

当通过键查找值时,HashMap 也会根据键的哈希值计算出数组下标,并查找对应的值。


原理层的内容

一、基本结构

在 Java 7 中,HashMap 底层由一个 数组(Entry[] table) 组成,其中每个数组元素称为一个 桶(Bucket)。 每个桶中存放的是一个 单向链表(即哈希冲突后同哈希值的元素被串联起来)。

结构示意如下:

table[0] -> Entry1 -> Entry2 -> Entry3
table[1] -> null
table[2] -> Entry4 -> Entry5
...

每个 Entry 节点的定义(Java 7)大致如下:

java
static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    final int hash;

    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }
}

二、哈希计算与寻址

HashMap 通过 key 的 hashCode() 来决定元素在数组中的存储位置。 但为了让哈希分布更均匀,Java 7 中还会对哈希值进行“扰动函数(hash扰动)”计算。

java
int hash = hash(key);
int index = (table.length - 1) & hash;

1. hash() 函数

在 Java 7 中:

java
final int hash(Object k) {
    int h = k.hashCode();
    return h ^ (h >>> 16);  // 高位扰动
}

这一步的目的是让高位参与低位运算,从而减少哈希冲突。

2. index计算

通过 & (length - 1) 替代 % length 来求模。 因为数组长度总是 2 的幂,用位运算效率更高。


三、哈希冲突与链表结构

当两个不同的 key 计算出的哈希索引相同,就会产生 哈希冲突(Hash Collision)

解决办法:链地址法(Separate Chaining)

即将新元素插入到对应桶的链表中。例如:

java
Entry<K,V> e = table[index];
table[index] = new Entry<>(hash, key, value, e);

在 Java 7 中,新元素插入链表的头部(头插法),这意味着:

  • 插入新节点时,原先的节点成为新节点的 next
  • 导致在多线程环境下扩容时容易出现环形链表问题(死循环)。

四、扩容机制(Rehashing)

1. 触发条件

当 HashMap 中的元素数量超过阈值(threshold)时,会触发扩容。 阈值 = capacity * loadFactor

默认:

java
initialCapacity = 16
loadFactor = 0.75

→ 阈值 = 16 × 0.75 = 12 即超过 12 个元素就会扩容。

2. 扩容过程

扩容时,HashMap 会创建一个新的数组(容量为原来的 2 倍),并将原有数据重新计算位置。

java
void resize(int newCapacity) {
    Entry[] oldTable = table;
    Entry[] newTable = new Entry[newCapacity];
    for (Entry<K,V> e : oldTable) {
        while (e != null) {
            Entry<K,V> next = e.next;
            int i = e.hash & (newCapacity - 1);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
    table = newTable;
}

这一步称为 Rehashing,因为每个元素都需要重新计算桶位置。 扩容开销较大,所以频繁扩容会严重影响性能。


五、Java 8 中的改进:红黑树(Treeify)

在 Java 8 中,为了解决链表过长导致性能下降的问题,引入了 红黑树结构

1. 触发条件

当某个桶中的链表长度超过 8,并且数组容量大于 64 时,会将链表转化为红黑树。

java
if (binCount >= TREEIFY_THRESHOLD - 1)
    treeifyBin(tab, hash);

2. 优势

  • 链表查找复杂度:O(n)
  • 红黑树查找复杂度:O(log n)

当哈希冲突严重时,性能大幅提升。

3. 节点类型变化

Java 8 引入了 Node(代替 Entry)和 TreeNode 两种结构:

java
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
}

当链表过长时,Node 会转换为 TreeNode,形成平衡二叉树结构。


六、线程安全问题

需要注意:

  • HashMap多线程环境下是不安全的
  • Java 7 使用头插法在扩容时可能导致链表成环,从而死循环;
  • Java 8 改为 尾插法,并优化了扩容逻辑,避免了死循环。

多线程环境建议使用:

  • ConcurrentHashMap
  • 或者通过 Collections.synchronizedMap() 包装。

七、总结对比(Java 7 vs Java 8)

特性Java 7Java 8
底层结构数组 + 链表数组 + 链表 + 红黑树
链表插入方式头插法尾插法
扩容时机超过阈值后同样,但逻辑优化
冲突处理链表链表 + 红黑树优化
hash扰动函数存在优化后更简单
线程安全不安全不安全(需 ConcurrentHashMap)

八、结语

Java 7 的 HashMap 是一个典型的哈希表实现,通过 数组 + 链表 实现高效的存取。 Java 8 在此基础上引入了 红黑树,有效解决了链表过长导致性能退化的问题。 理解 HashMap 的底层原理,不仅能帮助我们编写更高效的代码,也能在排查复杂问题(如扩容引发的死循环、哈希冲突)时提供理论支撑。

随便写写的,喜欢就好。 使用VitePress构建