面试官:看过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,即仅需键匹配即可删除
相关推荐
程序猿阿越7 小时前
Kafka源码(六)消费者消费
java·后端·源码阅读
zh_xuan6 天前
Android android.util.LruCache源码阅读
android·源码阅读·lrucache
魏思凡9 天前
爆肝一万多字,我准备了寿司 kotlin 协程原理
kotlin·源码阅读
白鲸开源13 天前
一文掌握 Apache SeaTunnel 构建系统与分发基础架构
大数据·开源·源码阅读
Tans523 天前
Androidx Fragment 源码阅读笔记(下)
android jetpack·源码阅读
Tans51 个月前
Androidx Fragment 源码阅读笔记(上)
android jetpack·源码阅读
Tans51 个月前
Androidx Lifecycle 源码阅读笔记
android·android jetpack·源码阅读
凡小烦1 个月前
LeakCanary源码解析
源码阅读·leakcanary
程序猿阿越1 个月前
Kafka源码(四)发送消息-服务端
java·后端·源码阅读
CYRUS_STUDIO1 个月前
Android 源码如何导入 Android Studio?踩坑与解决方案详解
android·android studio·源码阅读