首页 > 编程语言 >Java HashMap 源码解读笔记(二)--xunznux

Java HashMap 源码解读笔记(二)--xunznux

时间:2024-08-02 21:24:00浏览次数:15  
标签:Java HashMap 链表 源码 哈希 hash null 节点

文章目录

HashMap

本文主要是用于记录我在阅读Java1.8的 HashMap 源码所做的笔记。对于源码中的注释会进行翻译下来,并且会对其中部分源码进行注释。

这篇文章主要是关于插入键值对、扩容、树化和去树化方法的阅读。

putVal 插入新值方法

/**  
 * 实现 Map.put 和相关方法。  
 *  
 * @param 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) {
                // 遍历找到尾结点 p,e为待插入节点的位置
                if ((e = p.next) == null) {
                    // 尾插法
                    p.next = newNode(hash, key, value, null);
                    // 如果链表长度达到阈值,则将其转换为红黑树
                    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) { // existing mapping for key
            V oldValue = e.value;
             // 如果 onlyIfAbsent 为 false 或者旧值为 null,则更新值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e); // 执行节点访问后的操作
            return oldValue;  // 返回旧值
        }
    }
    // 修改HashMap 结构修改的次数,并检查是否需要扩容
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

方法解读

Java8之前的HashMap是采用头插法来解决hash冲突的,这意味着新插入的元素会被插入到链表的头部,而不是尾部。而Node数组存储的是链表的头节点。
Java8中将HashMap的插入方法改为了尾插法,也就是新插入的元素会被插入到链表的尾部。**这样做的主要原因是尾插法对于后续元素的查找和删除操作更加高效,因为新插入的元素在链表的尾部,因此只需要遍历链表一次就可以找到链表的尾部。**另外,Java8中还引入了红黑树来优化HashMap,在链表过长时会自动转换为红黑树,提高了查找的效率。
Java 8中将HashMap的插入方法改为了尾插法,提高了HashMap的性能,主要表现在以下两个方面:

  1. 避免了链表破坏带来的性能问题
    在Java 8之前,HashMap的插入方法是基于头插法实现的,即在链表的头部添加新元素,这样可以保证新添加的元素总是位于链表的头部,而旧的元素则位于链表的尾部。但是,在多线程并发情况下,可能会出现链表破坏的情况,即两个线程在链表的同一位置插入元素,导致链表数据结构被破坏,出现环形链表或链表断裂等问题。这样会导致HashMap的性能严重下降,甚至可能出现死循环等问题。
    而将HashMap的插入方法改为了尾插法,可以避免链表破坏带来的性能问题。因为在多线程并发情况下,新元素总是会被添加到链表的末尾,不会影响链表中已有元素的位置,从而避免了链表破坏带来的性能问题。
  2. 提高了查询性能
    在Java 8中,HashMap的插入方法改为了尾插法,这种实现方式可以提高HashMap的查询性能。具体来说,由于新元素总是被添加到链表的末尾,查询时可以更快地找到需要的元素。因为在遍历链表时,新元素总是位于链表的尾部,查询时可以更快地找到需要的元素。这样可以提高HashMap的查询效率,从而提高HashMap的性能。

1.7和1.8有哪些区别

  • JDK1.7的时候使用的是节点数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是节点数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,并且数组size大于64,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(n)变成O(logN)提高了效率)。因为当 table 数组容量比较小时,键值对节点 hash 的碰撞率可能会比较高,进而导致链表长度较长。这个时候应该优先扩容,而不是立马树化。
  • JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法。因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
  • 扩容后数据存储位置的计算方式也不一样:在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(hash值 & length-1)。而在JDK1.8的时候通过 hash & oldCap 的值来判断,并不需要重新如上一样再次计算。这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。

resize 重新哈希方法

高效的重新哈希:
当 HashMap 扩容时,它会尽量保持元素在新数组中的位置不变,或者只移动到新位置的固定偏移量处。这种做法减少了重新哈希的开销,并且保证了即使在扩容后也能快速定位元素。

/**
 * 初始化或翻倍哈希表的大小。如果哈希表为空,则根据初始容量目标分配新表。
 * 否则,因为使用的是二的幂次方扩展,所以每个桶中的元素要么保持在相同的位置,
 * 要么移动到新表中相应位置,偏移量为二的幂次方。
 *
 * @return 新的哈希表
 */
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; // 返回旧表
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && // 如果旧表容量翻倍后不超过最大容量
                 oldCap >= DEFAULT_INITIAL_CAPACITY) { // 并且旧表容量大于等于默认初始容量
            newThr = oldThr << 1; // 阈值翻倍
        }
    }
    else if (oldThr > 0) { // 如果旧表为空但阈值不为零(即设置了初始容量)
        newCap = oldThr; // 使用阈值作为新的容量
    }
    else { // 如果旧表为空并且阈值也为零(即使用默认设置)
        newCap = DEFAULT_INITIAL_CAPACITY; // 使用默认初始容量
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 计算阈值
    }

    if (newThr == 0) { // 如果阈值为零
        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) { // 遍历旧表
            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) { // 如果哈希值的低位为0
                            if (loTail == null) // 如果低索引部分还没有节点
                                loHead = e; // 设置头节点
                            else
                                loTail.next = e; // 连接到尾部
                            loTail = e; // 更新尾部
                        }
                        else { // 如果哈希值的低位为1
                            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; // 返回新表
}

resize() 方法是 HashMap 中用于初始化或扩容存储桶数组的核心方法。它的主要作用是根据当前的容量和负载因子,计算出新的容量和阈值,并创建一个新的存储桶数组,将原有的键值对重新散列到新的数组中。以下是对这个方法的详细解读:

  1. 首先获取当前的存储桶数组 oldTab、当前容量 oldCap 和当前阈值 oldThr。
  2. 根据 oldCap 的不同情况,计算新的容量 newCap 和新的阈值 newThr。
    • 如果 oldCap 大于 0,说明已经初始化过了:
    • 如果 oldCap 已经达到最大容量 MAXIMUM_CAPACITY,则将阈值设置为 Integer.MAX_VALUE,并返回原数组。
    • 否则,新容量 newCap 为 oldCap 的两倍,新阈值 newThr 也是 oldThr 的两倍。
    • 如果 oldCap 为 0,但 oldThr 不为 0,说明初始化时将初始容量设置在了 threshold 字段,此时 newCap 就等于 oldThr。
    • 如果 oldCap 和 oldThr 都为 0,说明是第一次使用 HashMap,此时 newCap 为默认初始容量 DEFAULT_INITIAL_CAPACITY,newThr 为默认初始容量乘以默认负载因子 DEFAULT_LOAD_FACTOR。
  3. 如果计算出的 newThr 为 0,则根据 newCap 和 loadFactor 重新计算 newThr。
  4. 创建一个新的存储桶数组 newTab,容量为 newCap。
  5. 如果 oldTab 不为空,则遍历 oldTab,将原有的键值对重新散列到 newTab 中。
    • 如果当前节点是单节点,则直接重新计算索引,将节点放入 newTab 对应的位置。
    • 如果当前节点是树节点(TreeNode),则调用 split 方法对树节点进行拆分,将拆分后的节点重新放入 newTab 中。
    • 如果当前节点是链表节点,则遍历链表,根据节点的哈希值是否大于等于 oldCap,将链表一分为二,分别放入 newTab 中对应的位置。
  6. 返回新的存储桶数组 newTab。

这个方法的主要逻辑是:
1. 根据当前容量和负载因子计算新的容量和阈值。
2. 创建新的存储桶数组。
3. 将原有的键值对重新散列到新的数组中,利用了 2 的幂次方扩容的特性,可以避免大量的节点位置重新计算。

resize() 方法保证了 HashMap 在元素数量增长时可以动态地调整存储桶的容量,从而保持较好的性能。同时,它也利用了红黑树来优化链表过长的情况,进一步提高性能。

treeifyBin 树化方法

/**
 * 将给定哈希值对应的链表转换为红黑树,除非哈希表太小,此时先进行扩容。
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; // 哈希表长度和索引
    Node<K,V> e; // 当前节点

    // 如果哈希表为空或长度小于最小树化阈值 64,则进行扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize(); // 扩容哈希表(扩容就需要重新哈希)
    else if ((e = tab[index = (n - 1) & hash]) != null) { // 如果该位置有节点
        TreeNode<K,V> hd = null, tl = null; // 红黑树的头节点和尾节点

        // 遍历链表,将每个节点转换为树节点
        do {
        	// replacementTreeNode 方法被用来将链表中的每个节点转换为 TreeNode。
            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); // 继续遍历

        // 将头节点放入哈希表中,并将其转换为红黑树
        if ((tab[index] = hd) != null)
            hd.treeify(tab); // 转换为红黑树
    }
}

replacementTreeNode 将链表中的每个节点转换为 TreeNode

// For treeifyBin
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
    return new TreeNode<>(p.hash, p.key, p.value, next);
}

treeify 树化方法

/**
 * 形成以当前节点为根的红黑树。
 */
final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null; // 初始化根节点为 null

    // 遍历从当前节点开始的链表
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next; // 获取下一个节点
        x.left = x.right = null; // 清空当前节点的左右子树

        // 如果根节点为空
        if (root == null) {
            x.parent = null; // 设置当前节点的父节点为空
            x.red = false; // 设置当前节点为黑色
            root = x; // 设置当前节点为根节点
        }
        else {
            K k = x.key; // 获取当前节点的键
            int h = x.hash; // 获取当前节点的哈希值
            Class<?> kc = null; // 初始化比较类为 null

            // 寻找插入位置
            for (TreeNode<K,V> p = root;;) {
                int dir, ph;
                K pk = p.key; // 获取当前遍历到的节点的键
                if ((ph = p.hash) > h) // 如果当前节点的哈希值大于待插入节点的哈希值
                    dir = -1; // 左转
                else if (ph < h) // 如果当前节点的哈希值小于待插入节点的哈希值
                    dir = 1; // 右转
                else if ((kc == null && // 如果比较类未初始化
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0) // 如果键相等
                    dir = tieBreakOrder(k, pk); // 解决键相等的情况

                TreeNode<K,V> xp = p; // 保存当前节点作为父节点
                if ((p = (dir <= 0) ? p.left : p.right) == null) { // 如果找到了插入位置
                    x.parent = xp; // 设置当前节点的父节点
                    if (dir <= 0) // 插入到左子树
                        xp.left = x;
                    else // 插入到右子树
                        xp.right = x;
                    root = balanceInsertion(root, x); // 平衡红黑树
                    break; // 结束循环
                }
            }
        }
    }
    moveRootToFront(tab, root); // 将根节点移到链表的前端
}

这段代码实现了将一个链表转换为红黑树的过程。具体来说,它首先检查是否需要创建一个新的红黑树(即根节点是否为空),然后遍历链表并将每个节点插入到红黑树中。在插入过程中,它会根据哈希值和键的比较结果来确定插入的方向,并在插入后对红黑树进行必要的平衡操作。最后,它会将根节点移到链表的前端,以便于后续的操作。

untreeify 链化方法

/**
 * 将从当前节点开始的红黑树转换回链表形式。
 * @param map 当前的 HashMap 实例
 * @return 返回一个链表形式的节点列表
 */
final Node<K,V> untreeify(HashMap<K,V> map) {
    Node<K,V> hd = null, tl = null; // 初始化头节点和尾节点为 null

    // 遍历红黑树中的节点
    for (Node<K,V> q = this; q != null; q = q.next) {
        Node<K,V> p = map.replacementNode(q, null); // 将树节点转换为链表节点

        // 如果尾节点为空,说明这是第一个节点
        if (tl == null)
            hd = p; // 设置头节点
        else
            tl.next = p; // 将新节点连接到尾节点之后

        tl = p; // 更新尾节点
    }

    return hd; // 返回头节点
}

这段代码实现的是将红黑树转换回链表的功能。当一个桶中的节点数量减少到一定阈值以下时(例如,当删除操作导致桶中的节点数量低于某个阈值时),HashMap 会调用此方法将红黑树转换回简单的链表形式。这样可以节省空间并减少维护红黑树所需的额外开销。

标签:Java,HashMap,链表,源码,哈希,hash,null,节点
From: https://blog.csdn.net/weixin_44615954/article/details/140879970

相关文章

  • Java HashMap 源码解读笔记(一)--xunznux
    文章目录HashMap介绍实现说明:源码解读静态常量和内部节点类Node静态工具方法属性字段Fields未完待续。。。HashMap本文主要是用于记录我在阅读Java1.8的HashMap源码所做的笔记。对于源码中的注释会进行翻译下来,并且会对其中部分源码进行注释。这一篇文章主要......
  • 【Java】Jsoup 解析HTML报告
    一、需求背景有好几种报告文件,目前是人肉找报告信息填到Excel上生成统计信息跟用户交流了下需求和提供的几个文件,发现都是html文件其实所谓的报告的文件,就是一些本地可打开的静态资源,里面也有js、img等等二、方案选型前面老板一直说是文档解析,我寻思这不就是写爬虫吗....因......
  • 基于Java养老院管理系统设计和实现(源码+LW+调试文档+讲解等)
    详细视频演示:请联系我获取更详细的演示视频系统技术介绍:后端框架SpringBootSpringBoot内置了Tomcat、Jetty和Undertow等服务器,这意味着你可以直接使用它们而不需要额外的安装和配置。SpringBoot的一个主要优点是它的自动配置功能。它可以根据你的项目中的依赖关......
  • javascript学习 - DOM 元素获取、属性修改
    什么是WebAPIWebAPI是指网页服务器或者网页浏览器的应用程序接口。简单来讲,就是我们在编写JavaScript代码时,可以通过WebAPI来操作HTML网页和浏览器。WebAPI又可以分为两类:DOM(文档对象模型)BOM(浏览器对象模型)DOM(DocumentObjectModel),即文档对象模型,主要用......
  • javascript学习 - DOM 事件
    事件什么是事件在之前DOM的学习中,我们主要学习了如何获取DOM元素,并且学会了如何给获取的元素进行属性修改等操作。但这些基本都是静态的修改,并没有接触到一些动作。而今天要学习的事件,其实就是这些动作的总称。所谓事件,就是在编程时系统内所发生的动作或者发生的事情......
  • Java学习记录
    对java进行配置通过下载jdk文件,然后在系统中设置环境变量,将新建变量JAVA_HOME,写入正确的jdk文件的路径接着在path中新建变量,将jdk的文件路径导入测试jdk是否安装成功:打开cmd在运行框输入cmd,如果显示如下信息则表示jdk安装成功Java语言的版本JavaSE​标准版,是为开发......
  • JavaScript 中的闭包和事件委托
    包(Closures)闭包是JavaScript中一个非常强大的特性,它允许函数访问其外部作用域中的变量,即使在该函数被调用时,外部作用域已经执行完毕。闭包可以帮助我们实现数据的私有化、封装和模块化,使代码更简洁、易读和可维护。闭包的定义简单来说,闭包是指有权访问另一个函数作用域中......
  • Dockerfile 构建java程序的docker镜像
    Dockerfile示例#设置jdk版本FROMopenjdk:8#设置容器内部工作目录为/java,后续命令将在该目录下执行操作WORKDIR/java#置容器的时区为亚洲/上海,以确保正确的时间设置。ENVTZ=Asia/Shanghai#在容器中设置正确的时区信息。RUNln-snf/usr/share/zoneinfo/$TZ/etc/local......
  • Springboot计算机毕业设计“漫画之家”系统+程序+源码
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表用户,漫画,同人插画,漫画活动,商品,约稿,约稿公告开题报告内容一、选题的背景及意义1.1背景随着科学技术的飞速发展和互联网的普及,漫画作为一种独特的艺术......
  • 苹果cmsv10酷黑模板模板 视频网站源码自适应模板【带有广告位】
    探索苹果CMSV10酷黑模板:打造高端自适应视频网站的完美选择在当今数字化时代,视频网站已成为人们休闲娱乐、获取信息的重要渠道之一。而一个精美、高效且用户友好的视频网站模板,则是吸引访客、提升用户体验的关键。今天,我们将带您深入了解苹果CMSV10的酷黑模板,这款集美观性、......