HashMap 原理

成员变量

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 数组默认长度

static final int MAXIMUM_CAPACITY = 1 << 30; // 数组长度最大值

static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认加载因子,数组长度达到 75% 就扩容

// TREEIFY_THRESHOLD、MIN_TREEIFY_CAPACITY 同时达到才会转红黑树
static final int TREEIFY_THRESHOLD = 8; // 添加元素,数组里的链表 >= 8 转为红黑树

static final int MIN_TREEIFY_CAPACITY = 64; // 添加元素,数组长度 >= 64 时,转为红黑树

static final int UNTREEIFY_THRESHOLD = 6; // 删除元素,数组里的链表默认 <= 6 时,如果是红黑树转为链表

transient Node<K,V>[] table; // 存储数据的数组

transient Set<Map.Entry<K,V>> entrySet; // 数据的集合

transient int size; // 元素个数

transient int modCount; // 修改次数

int threshold; // 扩容临界值,第一次 put 时初始化为 12,后面每次扩容维护一次

final float loadFactor; // 自定义的加载因子(如果创建实例时,指定了加载因子,默认的就不生效)

put 过程

假设是第一次 put ,只是看 put 过程,先不考虑 hash 碰撞、红黑树那些

// 入口方法
public V put(K key, V value) {
    // 先得到 key 的 hash 值
    return putVal(hash(key), key, value, false, true);
}

// putVal 过程
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) // 第一次进来 table 数组是空,
        n = (tab = resize()).length; // resize() 创建了一个长度是 16 的数组,扩容阈值是 12(虽然里面比较复杂,但是只看第一次添加,代码很简单就不贴了)
    // 此时数组下标是否为空,如果为空,床架一个节点,放入数组此下标
    if ((p = tab[i = (n - 1) & hash]) == null) // n 数组长度,再跟 key 的 hash 做与运算,这是在算 key 的下标
        tab[i] = newNode(hash, key, value, null);
    else {
		... 因为看第一次 put,这个 else 是不会走的
    }
    ++modCount; // 修改次数+1
    if (++size > threshold) // 维护 size,并判断是否需要扩容,达到扩容阈值没
        resize(); // 第一次添加,不会扩容
    afterNodeInsertion(evict); // LinkedHashMap 实现,跟 ArrayList 没关系
    return null;
}

hash 冲突

如果第二次添加,先假存在 hash 冲突,但是值不一样,因为如果值一样直接就覆盖完事儿了

// 还是 putVal 方法
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;
    if ((p = tab[i = (n - 1) & hash]) == null) // 哈希冲突,这时 key 算出来的下标在数组中就有值了,不会进这个判断
        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)))) // 先 key 的 hash 值,再用 equals 判断 value
            e = p; // 如果相等,直接覆盖
        else if (p instanceof TreeNode) // 已经存在的元素如果是树节点(已经是红黑树了)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 添加到树里里面去
        else { // 如果不是树节点,那就是链表
            for (int binCount = 0; ; ++binCount) {
                // p 是已存在的节点,p 的下一个节点如果为空
                if ((e = p.next) == null) { // 尾插法,8 之前是头插法(七上八下)
                    p.next = newNode(hash, key, value, null); // 把这次 put 的数据封装成节点,并作为 p 的下一个节点(HashMap 是单向链表)
                    // 8-1=7,下标是7,第八个元素,如果已经遍历到这里,那就说明可能要转成红黑色了(如果数组长度也达到64才会真正转)
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash); // 这个方法里面会判断数组长度是否达到 64,如果达到了,数据结构就转为红黑树
                    break; // 到这里就结束了,如果不用转红黑树,单向链表已经维护了,如果要转红黑树,也转了
                }
                // 如果 p 的下一个节点不为空才会这里,这里判断下一个节点,是否需要覆盖
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break; // 如果需要覆盖,直接 break?因为这个循环的第一个判断就把该节点指给了 e,所以 e 不为空,跳出后就执行覆盖逻辑了
                p = e; // 如果不需要覆盖,再把 e 指给 p 这个变量,然后进入链表的下一个节点遍历
            }
        }
        // 如果 e 不为空,就是 hashCode 和 equals 都相同,要覆盖值
        if (e != null) { // existing mapping for key
            V oldValue = e.value; // e 原来的数据
            // 原来的数据为空或onlyIfAbsent为false,就进这个判断,判断也很简单,就是把新的 value 设置为节点的值
            if (!onlyIfAbsent || oldValue == null) // onlyIfAbsent 传参进来的写死的是 false
                e.value = value; // put 带过来的 value 设置到节点的 value,完成值的覆盖
            afterNodeAccess(e); // LinkedHashMap 实现,跟 ArrayList 没关系
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict); // LinkedHashMap 实现,跟 ArrayList 没关系
    return null;
}

转红黑树

细节后面再揪,现在先点到为止吧。树的变种很多,二叉,多叉,二叉又分为完全、完满、平衡、AVL、红黑,目前还不具备这块能力 _

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) // 如果数组长度没有达到 64
        resize(); // 扩容,并不会转红黑树
    else if ((e = tab[index = (n - 1) & hash]) != null) { // 当前节点取出来赋值给 e,e 就是当前节点,这个链表的节点要维护成红黑树的节点
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null); // 这里把 e 转成红黑树节点
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

扩容

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) {
        // 如果数组长度已经达到 MAXIMUM_CAPACITY(最大值)
        if (oldCap >= MAXIMUM_CAPACITY) { 
            threshold = Integer.MAX_VALUE; 
            return oldTab; // 达到了最大值,不会再扩容,直接把原来的数组返回去
        }
        // 如果老的长度达到 16,新长度就是老长度的 2 倍,oldCap << 1 就是 * 2 的意思
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 如果新的长度没达到 MAXIMUM_CAPACITY,扩容阈值也扩大到两倍
            // 如果新长度没达到最大值,不设置扩容阈值,下一次再扩容的时候走上面的判断,赋值为 Integer.MAX_VALUE
            newThr = oldThr << 1; 
    }
    // 有参构造创建的 HashMap,长度设置为根据参数计算出来的扩容阈值
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr; 
    else {  // 无参构造创建的 HashMap,长度设置为 16,扩容阈值设置为 16*0.75 = 12
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) { // 带了参数创建的 HashMap 前面只设置了长度,这里设置下下一次的扩容阈值
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr; // 下一次的扩容阈值
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 用新的长度创建一个数组
    table = newTab; // 新数组赋值给 table
    if (oldTab != null) { // 如果老数据有数据,要把老数组的数据迁移到新数组
        for (int j = 0; j < oldCap; ++j) {
            // 因为长度变化了,原来数据对应的下标可能发生变化,会根据数组长度重新计算下标
            ... 具体代码就不贴了
        }
    }
    return newTab;
}

1.7 死链

出现在并发扩容(两个线程调用 transfer 方法,当一个已经完成扩容,另一个线程正在扩容中),jdk1.7 采用的是头插法,在极端情况会出现死链

  1. 比如数组下标为 1 的元素是个链表,存放的数据是 1 --> 35 --> 16 --> null
  2. 线程 A 线程、B 同时扩容
  3. 线程 A 拿到了 table[1] 的数据,先拿到 1,存放在线程 A 的局部变量,然后拿下一个元素 35 也放在局部变量...(假设线程 A 就刚好走在这里)
  4. 线程 B 先完成扩容,扩容后 table[1] 放的是 35 --> 1 --> null(16 假设放到另一个数组下标了,头插法所以 35 在 1 前面)
  5. 线程 A 已经拿到数据为:1 --> 35;继续拿下一个,此时线程 B 已经把 35 的下一个元素设置成了1,此时就产生死链了(下一个元素是自己的上一个元素)
posted @   CyrusHuang  阅读(27)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示