数据结构算法 - HashMap 源码解析

简介: 数据结构算法 - HashMap 源码解析

思考题:

equals 和 == 的区别,hashCode 与它们之间的联系?

HashMap 的长度为什么是 2 的幂次?

五个线程同时往 HashMap 中 put 数据会发生什么?

Hashmap中的hash冲突到底指的是什么?

Hashmap进行put操作的时候,会对key值进行比较吗?

HashMap中是采用的键值对的方式存储,那么put操作的时候是直接比较key值,相等覆盖,不等新增,怎么会出现线程不安全的情况?

HashMap什么情况下进行扩容?


一、初窥HashMap


HashMap是应用更广泛的哈希表实现,而且大部分情况下,都能在常数时间性能的情况下进行put和get操作。

要掌握HashMap,主要从如下几点来把握:

jdk1.7中底层是由数组(也有叫做“位桶”的)+链表实现;

jdk1.8中底层是由数组+链表/红黑树实现可以存储null键和null值,

线程不安全初始size为16,

扩容:newsize = oldsize*2,size一定为2的n次幂扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀.


1.8中HashMap结构图解:

4.png


Hashmap中的hash冲突到底指的是什么?


简单的说,我们向hashmap中put数据时,首先会根据key值的hashcode的值去bucket数组中进行快速选址找到对应的桶,当出现hashcode相等的情况,就是出现了hash冲突。


很多小朋友可能还是很迷惑,为什么叫Hash冲突呢,出现了hash冲突会导致什么问题?

首先一点,不同的key值会计算出相同的hashcode,这是产生hashcode根本的原因。

出现了hash冲突,就会导致,多个不同的key值,对应同一个桶。


那么为什么不让每个key值都计算出唯一的hashcode呢?

如果这样,我向hashmap中存1万个值,我的bucket数组的长度就有1万,每次根据key去取值的时候,要从这1万个数组元素中去取,查询效率可想而知。

这里的hashcode值,可以简单的想象成是根据key值和bucket数组的长度计算的模(可以理解成余数),根据这个模,找到存放entry的数组对应的index。(实际中更加复杂)

如果当前桶中是空的,则直接添加。

插入的时候,不会比较key值,只会比较key的hash值。


那么hashMap中是怎么解决hash冲突的呢?

hashmap中通过链表和红黑树来解决hash冲突。


为什么说HashMap是线程不安全的?

在接近临界点时,若此时两个或者多个线程进行put操作,都会进行resize(扩容)和reHash(为key重新计算所在位置),而reHash在并发的情况下可能会形成链表环。

个人见解:

在多个线程向hashmap中同一个空位插入数据时,刚好出现hash冲突,可能会出现相互覆盖的情况。


什么时候会进行扩容,会导致什么问题?


源码:

/**
     * The default initial capacity - MUST be a power of two.
     * 初始容积的大小16,必须是2的平方。这和操作系统的位移计算有关
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
  /**
     * The load factor used when none specified in constructor.
     * 负载因子,可以理解成扩容的阈值
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
   /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     *  当链表的长度大于这个值时,将链表转化为红黑树
     */
    static final int TREEIFY_THRESHOLD = 8;


核心put方法分析:


/**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }


final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
       //如果table为空或者长度为0,先进行扩展resize
        if ((tab = table) == null || (n = tab.length) == 0)
           //初次扩容n等于8
            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;
            //如果链表尾部元素的hash值和插入元素的hash值相等,且key的内存地址相等或key值相等,则e = p;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
               //如果p为树节点,则向红黑树中添加新节点e
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {//如果p不是树节点
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        //尾部节点的next添加新节点
                        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) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        #当链表的长度,大于threshold = loadFactor * 容量  时,进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }


如果存在Hash碰撞就会以链表的形式保存,把当前传进来的参数生成一个新的节点保存在链表的尾部(JDK1.7保存在首部)。而如果链表的长度大于8那么就会以红黑树的形式进行保存。


扩容机制核心方法Node<K,V>[] resize():

HashMap扩容可以分为三种情况:


第一种:使用默认构造方法初始化HashMap。从前文可以知道HashMap在一开始初始化的时候会返回一个空的table,并且thershold为0。因此第一次扩容的容量为默认值DEFAULT_INITIAL_CAPACITY也就是16。同时threshold = DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR = 12。


第二种:指定初始容量的构造方法初始化HashMap。那么从下面源码可以看到初始容量会等于threshold,接着

threshold = 当前的容量(threshold)* DEFAULT_LOAD_FACTOR。


第三种:HashMap不是第一次扩容。如果HashMap已经扩容过的话,那么每次table的容量以及threshold量为原有的两倍。


这边也可以引申到一个问题就是HashMap是先插入数据再进行扩容的,但是如果是刚刚初始化容器的时候是先扩容再插入数据。

目录
相关文章
|
6月前
|
算法 数据可视化 数据挖掘
基于EM期望最大化算法的GMM参数估计与三维数据分类系统python源码
本内容展示了基于EM算法的高斯混合模型(GMM)聚类实现,包含完整Python代码、运行效果图及理论解析。程序使用三维数据进行演示,涵盖误差计算、模型参数更新、结果可视化等关键步骤,并附有详细注释与操作视频,适合学习EM算法与GMM模型的原理及应用。
|
6月前
|
存储 监控 安全
企业上网监控系统中红黑树数据结构的 Python 算法实现与应用研究
企业上网监控系统需高效处理海量数据,传统数据结构存在性能瓶颈。红黑树通过自平衡机制,确保查找、插入、删除操作的时间复杂度稳定在 O(log n),适用于网络记录存储、设备信息维护及安全事件排序等场景。本文分析红黑树的理论基础、应用场景及 Python 实现,并探讨其在企业监控系统中的实践价值,提升系统性能与稳定性。
185 1
|
6月前
|
存储 监控 算法
基于跳表数据结构的企业局域网监控异常连接实时检测 C++ 算法研究
跳表(Skip List)是一种基于概率的数据结构,适用于企业局域网监控中海量连接记录的高效处理。其通过多层索引机制实现快速查找、插入和删除操作,时间复杂度为 $O(\log n)$,优于链表和平衡树。跳表在异常连接识别、黑名单管理和历史记录溯源等场景中表现出色,具备实现简单、支持范围查询等优势,是企业网络监控中动态数据管理的理想选择。
183 0
|
7月前
|
存储 安全 Java
Java 集合面试题从数据结构到 HashMap 源码剖析详解及长尾考点梳理
本文深入解析Java集合框架,涵盖基础概念、常见集合类型及HashMap的底层数据结构与源码实现。从Collection、Map到Iterator接口,逐一剖析其特性与应用场景。重点解读HashMap在JDK1.7与1.8中的数据结构演变,包括数组+链表+红黑树优化,以及put方法和扩容机制的实现细节。结合订单管理与用户权限管理等实际案例,展示集合框架的应用价值,助你全面掌握相关知识,轻松应对面试与开发需求。
365 3
|
10月前
|
算法 Java
算法系列之数据结构-Huffman树
Huffman树(哈夫曼树)又称最优二叉树,是一种带权路径长度最短的二叉树,常用于信息传输、数据压缩等方面。它的构造基于字符出现的频率,通过将频率较低的字符组合在一起,最终形成一棵树。在Huffman树中,每个叶节点代表一个字符,而每个字符的编码则是从根节点到叶节点的路径所对应的二进制序列。
312 3
 算法系列之数据结构-Huffman树
|
10月前
|
存储 自然语言处理 数据库
【数据结构进阶】AVL树深度剖析 + 实现(附源码)
在深入探讨了AVL树的原理和实现后,我们不难发现,这种数据结构不仅优雅地解决了传统二叉搜索树可能面临的性能退化问题,还通过其独特的平衡机制,确保了在任何情况下都能提供稳定且高效的查找、插入和删除操作。
779 19
|
10月前
|
算法 Java
算法系列之数据结构-二叉搜索树
二叉查找树(Binary Search Tree,简称BST)是一种常用的数据结构,它能够高效地进行查找、插入和删除操作。二叉查找树的特点是,对于树中的每个节点,其左子树中的所有节点都小于该节点,而右子树中的所有节点都大于该节点。
435 22
|
10月前
|
监控 算法 安全
基于 C# 的内网行为管理软件入侵检测算法解析
当下数字化办公环境中,内网行为管理软件已成为企业维护网络安全、提高办公效率的关键工具。它宛如一位恪尽职守的网络守护者,持续监控内网中的各类活动,以确保数据安全及网络稳定。在其诸多功能实现的背后,先进的数据结构与算法发挥着至关重要的作用。本文将深入探究一种应用于内网行为管理软件的 C# 算法 —— 基于二叉搜索树的入侵检测算法,并借助具体代码例程予以解析。
160 4
|
10月前
|
数据库 C++
【数据结构进阶】红黑树超详解 + 实现(附源码)
本文深入探讨了红黑树的实现原理与特性。红黑树是一种自平衡二叉搜索树,通过节点着色(红/黑)和特定规则,确保树的高度接近平衡,从而实现高效的插入、删除和查找操作。相比AVL树,红黑树允许一定程度的不平衡,减少了旋转调整次数,提升了动态操作性能。文章详细解析了红黑树的性质、插入时的平衡调整(变色与旋转)、查找逻辑以及合法性检查,并提供了完整的C++代码实现。红黑树在操作系统和数据库中广泛应用,其设计兼顾效率与复杂性的平衡。
2094 3
|
10月前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~

推荐镜像

更多
  • DNS