面试官:看过HashMap的源码吗?说说HashMap的原理

HashMap的构造函数

java 复制代码
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; 
}

HashMap的构造参数可以指定初始容量和负载因子,未指定时使用默认的初始容量和负载因子。默认初始容量为64,负载因子为75%。

当节点使用链表形式时,内部结构如下所示

java 复制代码
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;    // key的哈希值
    final K key;      // key的值
    V value;          //value的值
    Node<K,V> next;   // 指向下一个节点
}

当节点使用红黑树时,内部结构如下

java 复制代码
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
}

key的哈希值计算方式,由key对象的hashCode()并进行进一步处理方法进行计算

java 复制代码
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

常用方法

get()方法

java 复制代码
final Node<K,V> getNode(Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
    // 哈希表存在且不为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & (hash = hash(key))]) != null) { // 定位到该key在哈希表中的位置
        // 如果该位置第一个节点的hash和给出的Key的哈希值相等
        // 且这两个对象是一个对象或值相等,直接返回
        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            // 如果已经树化
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
               // 仍然为链表,直接向下查找
            do {
                if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

// 红黑树时的查找方法
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
    TreeNode<K,V> p = this;
    do {
        int ph, dir; K pk;
        TreeNode<K,V> pl = p.left, pr = p.right, q;
        if ((ph = p.hash) > h)
            p = pl;
        else if (ph < h)
            p = pr;
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
        else if (pl == null)
            p = pr;
        else if (pr == null)
            p = pl;
        else if ((kc != null ||
                  (kc = comparableClassFor(k)) != null) &&
                 (dir = compareComparables(kc, k, pk)) != 0)
            p = (dir < 0) ? pl : pr;
        else if ((q = pr.find(h, k, kc)) != null)
            return q;
        else
            p = pl;
    } while (p != null);
    return null;
}

我们看到判断是不是目标key不仅仅比较hash值,还要比较这两个key的对象或值是否相等。这也是我们为什么说HashCode()和equals()要一起重写。

例如:

java 复制代码
public int hashCode() {
    return 1;//h 为一个随机数
}

public boolean equals(Object o) {
    return true;
}

这种情况下,已经有一个[1,value]这样一个键值对,现在插入[2,value2]或任意键值对,这样就会直接认为这是同一个键值对,直接覆盖key为1的值。这与我们预期是不符的。

java 复制代码
public int hashCode() {
    return 1;//h 为一个随机数
}

public boolean equals(Object o) {
    return true;
}

在这种情况下,已经存在一个[1,value]这样一个键值对,现在插入[1,value2],因为equals()返回false,会认为这两个键是不一样的,会再插入一个键值对。

put()方法

java 复制代码
/**
 * 实现Map.put和相关方法的核心方法
 *
 * @param hash 键的哈希值(通过hash()方法计算得到)
 * @param key 要插入的键
 * @param value 要插入的值
 * @param onlyIfAbsent 如果为true,则不覆盖已存在的值
 * @param evict 如果为false,表示表处于创建模式
 * @return 返回旧值,如果没有则返回null
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    
    // 如果哈希表为空或长度为0,则进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    
    // 计算索引位置并检查是否为空
    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;
        // 如果是树节点,调用树节点的插入方法
        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);
                    // 链表长度达到树化阈值时转换为红黑树,树化阈值为8
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 找到已存在的键
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        
        // 如果键已存在,处理值的更新
        if (e != null) { 
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 访问后回调(LinkedHashMap会使用)
            afterNodeAccess(e);
            return oldValue;
        }
    }
    
    // 结构修改计数器,只有在插入之前没有存在该key时才会修改计数器,若已存在不会修改计数器
    ++modCount;
    
    // 检查是否需要扩容
    if (++size > threshold)
        resize();
    
    // 插入后回调(LinkedHashMap会使用)
    afterNodeInsertion(evict);
    return null;
}

// 树化查找方法
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                               int h, K k, V v) {
    Class<?> kc = null;  // 用于记录key的比较类型
    boolean searched = false;  // 是否已经搜索过树
    
    // 获取树的根节点
    TreeNode<K,V> root = (parent != null) ? root() : this;
    
    // 从根节点开始遍历
    for (TreeNode<K,V> p = root;;) {
        int dir, ph; K pk;
        
        // 比较当前节点与新节点的哈希值
        if ((ph = p.hash) > h)
            dir = -1;  // 新节点应在左子树
        else if (ph < h)
            dir = 1;   // 新节点应在右子树
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;  // 找到相同key的节点,直接返回
        
        // 哈希值相同但key不相等,使用比较器或类的自然顺序
        else if ((kc == null &&
                  (kc = comparableClassFor(k)) == null) ||
                 (dir = compareComparables(kc, k, pk)) == 0) {
            // 如果还没有搜索过,尝试在左右子树中查找
            if (!searched) {
                TreeNode<K,V> q, ch;
                searched = true;
                if (((ch = p.left) != null &&
                     (q = ch.find(h, k, kc)) != null) ||
                    ((ch = p.right) != null &&
                     (q = ch.find(h, k, kc)) != null))
                    return q;  // 在子树中找到节点
            }
            
            // 无法通过比较器决定顺序,使用默认比较方法
            dir = tieBreakOrder(k, pk);
        }

        TreeNode<K,V> xp = p;  // 保存当前节点作为父节点
        
        // 根据比较结果选择左子树或右子树
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            Node<K,V> xpn = xp.next;
            // 创建新的树节点
            TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
            
            // 将新节点插入到树中
            if (dir <= 0)
                xp.left = x;
            else
                xp.right = x;
            xp.next = x;
            x.parent = x.prev = xp;
            if (xpn != null)
                ((TreeNode<K,V>)xpn).prev = x;
                
            // 插入后调整树结构,保持红黑树平衡
            moveRootToFront(tab, balanceInsertion(root, x));
            return null;  // 插入成功,返回null
        }
    }
}

流程说明

  1. 哈希表初始化:如果哈希表为空或长度为 0,调用 resize () 方法进行初始化

  2. 计算索引 :通过(n-1) & hash计算键的存储位置

  3. 处理碰撞

    • 如果索引位置为空,直接创建新节点
    • 如果不为空,检查第一个节点是否匹配
    • 如果不匹配,检查是否为树节点
    • 如果不是树节点,遍历链表查找或插入新节点
    • 如果链表长度达到树化阈值(默认 8),将链表转换为红黑树
  4. 更新值:如果键已存在,根据 onlyIfAbsent 参数决定是否更新值

  5. 扩容检查:插入后检查元素数量是否超过阈值,超过则进行扩容

扩容方法resize()

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;
    
    // 1. 根据不同情况计算新容量和新阈值
    if (oldCap > 0) {
        // 情况1:旧容量已存在(非首次扩容)
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 已达到最大容量,无法扩容,将阈值设为Integer.MAX_VALUE
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 正常扩容:容量翻倍(左移1位),阈值也翻倍
            newThr = oldThr << 1; 
    }
    else if (oldThr > 0) 
        // 情况2:旧容量为0,但阈值>0(使用带初始容量的构造函数时)
        newCap = oldThr;
    else {               
        // 情况3:使用无参构造函数,使用默认值
        newCap = DEFAULT_INITIAL_CAPACITY; // 默认16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 16*0.75=12
    }
    
    // 2. 处理新阈值计算
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    
    // 3. 创建新哈希表数组
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    
    // 4. 迁移旧哈希表中的元素
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null; // 释放旧表的引用,帮助GC
                
                // 情况1:桶中只有一个元素,直接计算新位置
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                    
                // 情况2:桶中为树节点,调用树的split方法
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    
                // 情况3:桶中为链表,需要重新分配
                else { 
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    
                    // 遍历链表,根据hash值将节点分为两组
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            // 低位链表:原位置
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            // 高位链表:原位置+oldCap
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    
                    // 将分组后的链表放入新表
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

容量增长机制:每次扩容为原容量的2倍,阈值也相应调整为原阈值的2倍

  1. 不同类型的元素迁移优化
    • 单节点:直接计算新位置
    • 树节点 :调用split()方法,可能会拆分红黑树或退化为链表
    • 链表节点 :采用高低位分组策略,避免重新计算哈希值
      • (e.hash & oldCap) == 0的节点留在原位置
      • 否则放在原位置+oldCap的新位置
      • 这种方式确保迁移后链表元素顺序不变
  2. 性能优化点
    • 使用 2 的幂作为容量,保证(n-1) & hash等同于取模运算
    • 扩容时不需要重新计算每个元素的哈希值,只需判断新增的位是 0 还是 1
    • 链表迁移采用尾插法,避免了 JDK 7 中头插法导致的死循环问题

树化方法

java 复制代码
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    
    // 1. 检查哈希表容量是否小于最小树化容量(64)
    // 如果容量不足,则优先扩容而不是树化
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    
    // 2. 容量足够时进行树化
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;  // 树节点的头指针和尾指针
        
        // 3. 将链表节点转换为树节点,并保持原链表顺序
        do {
            // 创建树节点(保留原节点的哈希值、键和值)
            TreeNode<K,V> p = replacementTreeNode(e, null);
            
            // 构建双向链表结构(树节点同时维护链表指针)
            if (tl == null)
                hd = p;  // 头节点
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        
        // 4. 将新构建的树节点链表放入桶中,并调用treeify()方法转换为红黑树
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}
  1. 容量检查机制
    • 树化条件 :链表长度达到TREEIFY_THRESHOLD(默认 8)
    • 容量阈值 :哈希表总容量需达到MIN_TREEIFY_CAPACITY(默认 64)
    • 优先扩容:若容量不足,即使链表长度达标也会触发扩容而非树化 目的:通过扩容减少哈希冲突,可能使链表长度自然下降
  2. 节点转换过程
    • 将普通链表节点(Node)转换为树节点(TreeNode
    • 转换时保留原链表的顺序(通过nextprev指针)
    • 树节点同时维护树结构(父节点、左右子节点)和链表结构
  3. 红黑树构建
    • hd.treeify(tab) 方法将双向链表转换为红黑树
    • 红黑树的插入和平衡操作会调整节点顺序,但链表指针(next/prev)保持不变
    • 树化后,桶的第一个元素是红黑树的根节点,但仍可通过链表顺序遍历所有节点
  4. 为什么需要同时维护树和链表结构?
    • 链表顺序用于迭代器遍历(如keySet().iterator()
    • 树结构用于快速查找(O (log n) 时间复杂度)
    • 当元素被删除导致树节点过少时,可能会退化为链表(untreeify()方法)

删除方法remove()

java 复制代码
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    
    // 1. 检查哈希表是否为空且对应桶是否有元素
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        
        Node<K,V> node = null, e; K k; V v;
        
        // 2. 检查第一个节点是否匹配
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        
        // 3. 第一个节点不匹配,遍历后续节点
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                // 树节点:调用红黑树查找方法
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                // 链表节点:遍历链表查找
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;  // 保存当前节点的前驱节点
                } while ((e = e.next) != null);
            }
        }
        
        // 4. 找到目标节点后,检查是否需要值匹配
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            
            // 5. 根据节点类型执行删除操作
            if (node instanceof TreeNode)
                // 树节点:调用红黑树删除方法
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                // 链表头节点:直接更新桶的引用
                tab[index] = node.next;
            else
                // 链表中间节点:删除当前节点
                p.next = node.next;
                
            // 6. 更新计数器并回调
            ++modCount;
            --size;
            afterNodeRemoval(node);  // LinkedHashMap会使用
            return node;
        }
    }
    return null;  // 未找到节点
}
  1. 节点定位
    • 通过(n-1) & hash计算桶索引(利用 2 的幂特性优化取模)
    • 首先检查桶的第一个节点,多数情况下冲突较少,第一个节点即为目标
  2. 链表与树的处理
    • 链表 :遍历链表节点,通过==equals()比较键
    • 红黑树 :调用getTreeNode()方法,利用树的结构快速查找(O (log n))
  3. 删除操作
    • 链表头节点:直接将桶指向原头节点的下一个节点
    • 链表中间节点 :调整前驱节点的next指针跳过当前节点
    • 树节点 :调用removeTreeNode(),可能触发树的平衡调整或退化为链表
  4. 红黑树的特殊处理
    • 树节点删除后,可能需要通过旋转和颜色调整维持红黑树性质
    • 如果删除后树节点过少(小于 6 个),会触发树退化为链表(untreeify()
  5. 值匹配选项
    • matchValue参数控制是否需要同时匹配键和值
    • 默认为false,即仅需键匹配即可删除
相关推荐
sophie旭2 天前
《深入浅出react开发指南》总结之 10.1 React运行时总览
前端·react.js·源码阅读
塞尔维亚大汉16 天前
鸿蒙内核源码分析(并发并行篇) | 听过无数遍的两个概念
harmonyos·源码阅读
塞尔维亚大汉16 天前
鸿蒙内核源码分析——(自旋锁篇)
harmonyos·源码阅读
SunStriKE19 天前
veRL代码阅读-1.论文原理
深度学习·强化学习·源码阅读
CYRUS_STUDIO20 天前
逆向 JNI 函数找不到入口?动态注册定位技巧全解析
android·逆向·源码阅读
塞尔维亚大汉20 天前
鸿蒙内核源码分析(用栈方式篇) | 程序运行场地谁提供
harmonyos·源码阅读
易保山1 个月前
聊聊 Glide | 不看源码,只聊设计
开源·源码阅读·glide
F_Director1 个月前
傻子都能理解的 React Hook 闭包陷阱
前端·react.js·源码阅读
阿迪卡多1 个月前
5.5 接收器遮挡 (Receiver Shading) 详细解析
源码阅读