哈希表和 Java 的前世今生(中),掌握HashMap看这一篇就够了!!!

哈希表和 Java 的前世今生(上),掌握HashMap看这一篇就够了!!! 中,我们讲解了哈希表的原理以及JDK7 HashMap的源码及JDK7中HashMap的注意点。接下来,我们继续我们上次的讲解,本讲的重点是JDK8 HashMap!

四、JDK8 HashMap

4.1 问题 4:JDK8 中 HashMap 的变化

  • 结构变化:由数组+链表(JDK7)变成了数组+链表+红黑树(JDK8)。

在这里插入图片描述

  • 链表长度>=8,转换为红黑树;链表长度减少为 6,红黑树再变回链表。只有总的节点数量>=64 的时候,才会有红黑树,否则直接进行主数组扩容

  • 链表节点为 Node,红黑树节点为 TreeNode。Node 是 TreeNode 的父类。

在这里插入图片描述

  • 添加到链表后面:JDK7 中新的节点是加到最前,JDK8 后新节点是加到最后(也是避免死循环的一种解决方式)
    在这里插入图片描述

  • 主数组的创建不是构造方法中搞定,而是 put 元素时通过 resize()搞定

  • 哈希表扩容后原来链表节点重新散列后不改变之前顺序,也不会形成循环链表
    注意:JDK8 HashMap 虽然针对 JDK7 的缺点做了某些修改,但是仍旧是线程不安全的,并发情况下建议使用 ConcurrentHashMap,或者使用 Collections 加锁。

4.2 问题 5:为什么是当链表长度>=8 后变成红黑树,而不是其他值

在这里插入图片描述

因为泊松分布 Poisson distribution(概率和数理统计内容)。
在理想的随机 hashCodes 下,容器中节点的频率遵循泊松分布,对于 0.75 的默认调整阈值,泊松分布的概率质量函数中参数λ(事件发生的平均次数)的值约为 0.5,尽管λ的值会因为 load factor 值的调整而产生较大变化。

在这里插入图片描述
即链表中出现 8 个节点的概率是非常低的,仅有 0.00000006,所以不用担心产生大量红黑树导致结构复杂的问题

4.3 源码阅读(JDK8 HashMap)

  • 依旧要求主数组容量还是 2 的幂
    在这里插入图片描述
    这是一个小巧但精妙的方法,这里通过异或的位运算将两个字节的 n 打造成比cap 大但最接近 2 的 n 次幂的一个数值。例如:

在这里插入图片描述

  • 计算哈希码的方法简单了
    在这里插入图片描述

JDK7 中,hash 计算的时候会对操作数进行右移操作,计算复杂,目的是将高位也参与运算,减少 hash 碰撞;在 JDK8 中,链表可以转变成红黑树,所以 hash计算也变得简单。下面的图为 JDK8 中的 hash 计算和索引计算。
在这里插入图片描述

  • 添加数据的步骤:put()

在这里插入图片描述
图片来自:https://blog.csdn.net/goosson/article/details/81029729

put源码分析:

   public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    // 第三个参数 onlyIfAbsent 如果是 true,那么只有在不存在该 key 时或者 value 是 null 才会进行
// put 操作,第四个参数 evict 我们这里不关心
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K, V>[] tab;//指向哈希表主数组的数组名
        Node<K, V> p;//链表
        int n, i; //n 永远存放数组长度,i 表示 key 在数组中的索引
// 第一次 put 值的时候,会触发下面的 resize(),第一次 resize 和后续的扩容有些不一样,
// 因为这次是数组从 null 初始化到默认的 16 或自定义的初始容量
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
// 找到具体的数组下标,如此位置没有值,那么直接初始化一下 Node 并放置在这个位置就可以了
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {// 数组该位置有数据
            Node<K, V> e;
            K k;
// 首先,判断该位置的第一个数据和要插入的数据,key 是不是"相等",如果是,取出这个节点
            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) {
// 如果不存在相同 key 的,插入到链表的最后面(Java7 是插入到链表的最前面)
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
// TREEIFY_THRESHOLD 为 8,所以,如果新插入的值是链表中的第 8 个
// 会触发下面的 treeifyBin,也就是将链表转换为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
// 如果在该链表中找到了"相等"的 key(== 或 equals)
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
// 此时 break,那么 e 为链表中[与要插入的新值的 key "相等"]的 node
                        break;
                    p = e;
                }
            }
// e!=null 说明存在旧值的 key 与要插入的 key"相等"
// 对于我们分析的 put 操作,下面这个 if 其实就是进行 "值覆盖",然后返回旧值
            if (e != null) {
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
// 如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }


  • 主数组容量扩容为原来的二倍,原来元素的索引会有什么变化吗?
    索引要么是原来的索引,要么是原来的索引+原来的数组容量。在 JDK8 中,不进行存储位置的重新计算,而是判断应该在原位置还是新位置。
    判断条件为:
if ((e.hash & oldCap) == 0) {
	//如果是原来的索引
	} else{
	//如果是原来的索引+原来的数组容量
}

在这里插入图片描述

  • 扩容到底是怎么实现的 ?
    扩容后,原来的一个链表可能会变成两个链表。实现思路是定义四个指针,在对原来链表进行逐个重新散列的过程中
    1.Node loHead,loTail 分别指向索引不变的新链表的首节点、尾节点
    2.Node hiHead,hiTail 分别指向索引改变的新链表的首节点、尾节点
    在这里插入图片描述

  • 单链表变成红黑树是怎么实现的
    简单来说,是先调用 treeifyBin()方法,将单链表变成双向链表(但节点已经是红黑树的节点 TreeNode),再将双向链表转换为红黑树。

在这里插入图片描述

发布了114 篇原创文章 · 获赞 34 · 访问量 9694

猜你喜欢

转载自blog.csdn.net/JAVA_I_want/article/details/105386759