一篇文章带你弄懂HashMap底层原理

HashMap 是什么?

HashMap 是 Java 中常用的集合类,属于 Map 接口的实现,用来存储键值对。它的特点是基于哈希表实现,具备以下特性:

  • 键值对的存储 :每个键(key)对应一个值(value)。
  • 快速查询 :通过哈希算法进行快速查询,HashMap 查找、插入、删除操作的时间复杂度在理想情况下是 O(1)。
  • 允许 null 值HashMap 允许键和值为 null,但键只能有一个 null

HashMap 的底层结构

HashMap 底层是基于数组 + 链表 + 红黑树的组合实现的,主要包括以下几部分:

  1. 数组(Node<K,V>[] table)HashMap 的核心是一个数组,数组中的每个元素是一个链表或树的头节点,称为"桶"。
  2. 链表:当多个键通过哈希碰撞(Hash Collision)映射到同一个位置时,这些键会以链表的形式存储。
  3. 红黑树:当链表长度超过一定阈值(默认是8)时,链表会转换成红黑树,以提高查询效率。

HashMap 的工作原理

  1. 存储过程

    • 通过 hash(key) 方法计算键的哈希值,并通过 indexFor(hash, table.length) 方法确定该键值对应的数组索引。
    • 如果该索引位置没有元素,则将键值对直接存入该位置。
    • 如果发生哈希碰撞(两个不同的键计算出相同的索引),则将新键值对以链表的形式追加到该索引的链表中。
    • 当链表长度超过阈值 8 时,链表会转化为红黑树。
  2. 扩容

    • HashMap 的初始容量默认为 16,负载因子为 0.75。当数组元素达到容量的 75% 时(即 threshold = capacity * load factor),HashMap 会自动扩容,将数组容量翻倍,并重新计算每个元素的位置。
  3. 查找过程

    • 通过键的哈希值确定索引位置。
    • 若数组中对应位置存在链表或红黑树,则根据键的 equals() 方法遍历链表或树,找到对应的键值对。

HashMap 的重要特性

  • 哈希冲突:即两个或多个不同的键计算出相同的哈希值,这会导致多个键映射到数组的同一个索引。通过链表或红黑树解决冲突问题。
  • 红黑树优化:当链表长度超过 8 时,链表会转化为红黑树,查找效率从 O(n) 提高到 O(log n)。
  • 线程不安全HashMap 是非线程安全的。如果需要在多线程环境下使用 HashMap,需要通过同步机制(如 Collections.synchronizedMap())或使用线程安全的类(如 ConcurrentHashMap)。

红黑树的特点

红黑树是 HashMap 在链表长度超过一定阈值时使用的树结构。它是一种自平衡的二叉搜索树,具有以下特性:

  • 每个节点要么是红色,要么是黑色
  • 根节点是黑色
  • 每个叶子节点都是黑色的空节点(NIL 节点)
  • 红色节点的子节点必须是黑色的(不能有两个连续的红色节点)。
  • 从任意节点到其每个叶子节点的所有路径都包含相同数量的黑色节点

红黑树通过这些规则保证了树的平衡性,使得最坏情况下的查找、插入、删除操作的时间复杂度为 O(log n)。

HaspMap底层源码分析

HashMap 是 Java 中常用的数据结构,它以键值对 的形式存储数据,并通过哈希表 的形式快速查找键对应的值。为了更深入地理解 HashMap 的实现机制,我们需要从它的底层源码出发,分析其数据结构、核心方法和优化策略。

1. 基本数据结构

HashMap 中,最核心的两个数据结构是:

  • 数组Node<K,V>[] table,这是 HashMap 的底层存储结构,所有键值对都存储在数组中。
  • 链表和红黑树:当出现哈希冲突时,多个键值对会被存储在同一个数组位置(桶)中,最初使用链表实现,当链表长度超过一定阈值时(8),会转换为红黑树。
java 复制代码
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next; // 指向下一个节点
}

每个 Node 节点包含 4 个属性:

  • hash: 键的哈希值,用来确定元素存储的位置。
  • key: 键。
  • value: 值。
  • next: 指向下一个节点(用于解决哈希冲突时的链表)。
2. 核心字段
  • DEFAULT_INITIAL_CAPACITY:默认初始容量为 16。
  • DEFAULT_LOAD_FACTOR:默认负载因子为 0.75。
  • threshold :扩容阈值,等于 容量 * 负载因子,当元素数量超过该值时触发扩容。
  • size :当前 HashMap 中键值对的数量。
3. 核心方法
3.1. put(K key, V value) ------ 插入数据

put 方法用于将键值对存入 HashMap 中。如果键已经存在,则更新对应的值;如果键不存在,则新插入一条记录。

java 复制代码
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
  • hash(key):通过计算哈希值,将键均匀分布在数组的不同位置。
  • putVal:执行实际的插入逻辑。

putVal 方法逻辑

java 复制代码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;  // 初始化或扩容
    
    // 计算索引 i,并判断 i 位置是否已有节点
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null); // 没有节点,直接插入
    else { 
        Node<K,V> e; K k;
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p; // key 已经存在,记录节点
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 红黑树处理
        else { 
            // 链表处理哈希冲突
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null); // 插入链表尾部
                    if (binCount >= TREEIFY_THRESHOLD - 1) // 链表长度 >= 8,转换为红黑树
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break; // 处理key重复的情况
                p = e;
            }
        }
        
        // key重复时,更新旧值
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize(); // 如果 size 超过阈值,进行扩容
    return null;
}
  • 计算哈希值和索引 :通过 (n - 1) & hash 计算数组中的位置(i)。
  • 存储键值对 :如果位置 i 为空,直接存储;否则通过链表或红黑树处理哈希冲突。
  • 扩容 :如果元素数量超过阈值,则调用 resize() 方法进行扩容。
3.2. resize() ------ 扩容

扩容是 HashMap 中一个重要的机制。每次扩容时,容量会翻倍,旧数组中的元素会重新分配到新的数组位置上。

java 复制代码
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;

    // 计算新容量和阈值
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        newCap = oldCap << 1; // 容量翻倍
        newThr = oldThr << 1; // 阈值翻倍
    } else if (oldThr > 0) { 
        newCap = oldThr;
    } else { 
        newCap = DEFAULT_INITIAL_CAPACITY; 
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;

    // 重新哈希并迁移旧数据
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e; // 无链表/树,直接迁移
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 红黑树分割处理
                else { 
                    // 链表的重新分配
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        } else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    
                    if (loHead != null) newTab[j] = loHead;
                    if (hiHead != null) newTab[j + oldCap] = hiHead;
                }
            }
        }
    }
    threshold = newThr;
    return newTab;
}

扩容流程:

  • 新建一个容量为旧容量 2 倍的数组。
  • 遍历旧数组,将旧数据重新计算索引并迁移到新数组中。
  • 扩容后,哈希冲突的节点会重新分布,减少链表长度。
4. 红黑树转换

HashMap 在解决哈希冲突时,采用链表存储冲突的键值对。当链表长度过长时(超过 8),为了优化查询性能,会将链表转换成红黑树。

java 复制代码
void treeifyBin(Node<K,V>[] tab, int hash) {
    // 转换链表为红黑树的逻辑
}

红黑树的优势在于即使在最坏情况下,插入、删除和查找的时间复杂度都为 O(log n),显著提高了性能。

5. 总结
  • HashMap 通过数组、链表和红黑树实现键值对的存储,能够在大部分情况下提供 O(1) 的查询和插入效率。
  • 哈希冲突通过链表解决,链表过长时通过红黑树优化性能。
  • HashMap 采用负载因子和扩容机制,以减少哈希冲突并保持高效操作。
  • 红黑树的引入避免了极端情况下链表过长导致的性能问题,保证了 HashMap 的高效性。

寻址算法

HashMap 的寻址算法是通过计算键的哈希值来定位键值对在数组中的存储位置。这个过程可以分为三个主要步骤:

1. 计算哈希值

每个键(key)都有自己的哈希码 (hashCode),这是一个整数。HashMap 通过 hash() 方法对键的 hashCode() 进行处理,使哈希值更加均匀分布,减少冲突。具体做法是通过扰动函数将哈希值的高位和低位进行混合:

java 复制代码
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • key.hashCode() 计算键的哈希码。
  • h ^ (h >>> 16):通过异或运算,将哈希值的高位和低位混合,确保更多位数参与索引计算,减少哈希冲突。

2. 计算数组索引

计算出哈希值之后,HashMap 通过位运算将哈希值映射到数组的索引上,公式为:

java 复制代码
index = (n - 1) & hash
  • nHashMap 数组的长度,通常是 2 的幂(如 16、32 等)。
  • n - 1 生成一个掩码,它确保索引在数组的有效范围内。
  • & 按位与操作,只保留哈希值的低位,使其落在 [0, n-1] 的范围内,得到一个有效的数组索引。

举例 :如果数组长度 n = 16,某个键的 hash() 计算结果为 hash = 357,则 index = (16 - 1) & 357 = 15 & 357 = 5,所以该键值对会存储在数组索引为 5 的位置。

3. 处理哈希冲突

当两个不同的键计算出的索引相同,发生了哈希冲突HashMap 使用两种方式解决:

  • 链表:在同一个索引位置,以链表形式存储多个键值对。当插入时新元素会加到链表尾部,查找时遍历链表。
  • 红黑树:如果链表长度超过阈值(默认8),会将链表转换为红黑树,提升查找、插入、删除的效率。

4. 扩容与再哈希

HashMap 的容量达到一定比例(默认是负载因子 0.75),会进行扩容 ,即将数组长度加倍,并重新计算每个键值对的新位置。这一过程被称为再哈希,目的是减少哈希冲突,提高性能。

逻辑总结

  1. 计算哈希值 :根据键的 hashCode() 通过扰动函数计算出哈希值。
  2. 映射到数组索引 :通过 (n - 1) & hash 计算出键值对的存储位置。
  3. 处理哈希冲突:若冲突,使用链表或红黑树解决。
  4. 扩容与再哈希 :当达到负载因子时,HashMap 扩容并重新计算索引位置。
相关推荐
西几1 小时前
代码训练营 day48|LeetCode 300,LeetCode 674,LeetCode 718
c++·算法·leetcode
liuyang-neu1 小时前
力扣第420周赛 中等 3324. 出现在屏幕上的字符串序列
java·算法·leetcode
想做白天梦2 小时前
双向链表(数据结构与算法)
java·前端·算法
小卡皮巴拉2 小时前
【力扣刷题实战】相同的树
c语言·算法·leetcode·二叉树·递归
zyhomepage2 小时前
科技的成就(六十四)
开发语言·人工智能·科技·算法·内容运营
想做白天梦2 小时前
多级反馈队列
java·windows·算法
潇雷2 小时前
算法Day12|226-翻转二叉树;101-对称二叉树;104-二叉树最大深度;111-二叉树最小深度
java·算法·leetcode
爱编程— 的小李3 小时前
开关灯问题(c语言)
c语言·算法·1024程序员节
韭菜盖饭3 小时前
LeetCode每日一题3211---生成不含相邻零的二进制字符串
数据结构·算法·leetcode
极客代码3 小时前
C/C++ 随机数生成方法
c语言·开发语言·c++·算法