前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布

HashMap

原创
作者头像
Get
发布2024-04-17 00:13:02
650
发布2024-04-17 00:13:02

HashMap的实现原理:

代码语言:java
复制
JDK1.6、JDK1.7:HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。
                但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。
JDK1.8:HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
HashMap是基于哈希算法实现的,我们通过put(key,value)存储,get(key)来获取数据。
存储:put(key,value)
===========================================================================================================================================
0、当添加一个元素(key-value),HashMap 根据 key.hashCode() 计算出key的hash值。
1、根据 hash值确定插入数组中的位置,将该元素(key-value)存放在该位置,但有可能数组中的该位置已被同hash值得元素占用了(hash冲突:hash值相同,key值不同)。
2、这时就将该元素添加在同一hash值的元素的后面,他们在数组的同一位置,但是形成了链表,同一各链表上的Hash值是相同的,所以说数组存放的是链表。
3、当链表长度太长时,链表就转换为红黑树,这样大大提高了查找的效率。
===========================================================================================================================================
1、HashMap 的每一个元素,都是链表的一个节点(entry)。
2、新增一个元素时,会先计算 key 的 hash 值,找到存入数组的位置。
3、如果该位置已经有节点(链表头),则存入该节点的最后一个位置(链表尾)。
4、所以 HashMap 就是一个数组(bucket),数组上每一个元素都是一个节点(节点和所有下一个节点组成一个链表)或者为空,显然同一个链表上的节点 hash 值都一样。
===========================================================================================================================================
1、先获取key对象的hashcode。
2、根据hashcode计算出hash值
3、根据对应的hash值来将Entry对象放到table数组中,如果对应的索引没有Entry对象,就直接存进数组中,如果对应索引位置已经有Entry对象,则将已有Entry对象的next指向本Entry对象,形成链表。
===========================================================================================================================================
1、table未初始化或者长度为0,进行扩容
2.1、数组长度和元素哈希值做&操作,确定元素存放在哪个桶中,桶为空,新生成结点放入桶中
2.2、数组新增元素,判断是否达到临界值,达到则扩容、2倍
3、桶中已经存在元素:p(指向链表头节点)
3.1、key相等:比较桶中第一个元素p与要插入元素的key是否相等,key相等则此次put为覆盖操作
3.2、key不相等;头节点p为红黑树结点,则此次put为向红黑树中插入节点
3.3、key不相等;头节点p为链表结点,则此次put为向链表中插入节点(树化发生在此处)
	 第一种:已经遍历到尾部(没有相同的key),在最后插入新节点跳出,因节点数量+1,判断是否需要树化,需要则树化
	 第二种:e指向的节点与要插入节点的key相同,此次put为覆盖操作
2.2 处扩容机制:  
1、如果旧表的长度不是空
1.1、若已经到了最大容量,这时还要往map中放数据,则阈值设置为整数的最大值 
1.2把新表的长度设置为旧表长度的两倍,newCap=2*oldCap
2、如果旧表的长度的是0
2.1、旧阈值>0 则将阈值赋值给新表长度,因为在构造方法中是将capacity赋值给了threshold。 
2.2、旧阈值=0 则阈值使用默认值 
3、新阈值=0时,为新阈值赋值
4、下面开始构造新表,初始化表中的数据
4.1、遍历原来的旧表,移到新表中	
4.2、判断表(数组)中元素是否为空
4.2.1、判断当前表元素是否存下一个节点(链表),不存在则直接将该元素放在新表中
4.2.2、判断当前表元素是否存在树
4.2.1.1 如果当前元素(e后边有链表)存在链表带着个单链表,需要遍历单链表,将每个节点重新放在新表中( 新计算在新表的位置,并进行搬运)
5、循环判断下一个节点是否为空,同时为该链表进行分队 	
===========================================================================================================================================
HashMap:数组(bucket)+链表(或红黑树)组成
HashMap 的每一个元素,都是链表的一个节点(Entry:(hash、key、value,next))
当链表数组的容量超过初始容量的0.75时,再散列将链表数组扩大2倍,把原链表数组的搬移到新的数组中。

即HashMap的原理图是:

代码语言:java
复制
1、HashMap的底层数据结构?
   数组+单向链表+红黑树
2、HashMap的主要参数都有哪些?
   默认初始容量:16;负载因子(填充比):0.75
3、HashMap是怎么处理hash碰撞的?
   hashCode值相同,替换旧值、插入链表、插红黑树
4、hash的计算规则?
   index的计算公式:index = HashCode(Key) & (Length- 1)
5、HashMap的存取原理?
       1.计算关于key的hashcode值(与Key.hashCode的高16位做异或运算)
       2.如果散列表为空时,调用resize()初始化散列表
       3.如果没有发生碰撞,直接添加元素到散列表中去
       4.如果发生了碰撞(hashCode值相同),进行三种判断
           4.1:若key地址相同或者equals后内容相同,则替换旧值
           4.2:如果是红黑树结构,就调用树的插入方法
           4.3:链表结构,循环遍历直到链表中某个节点为空,尾插法进行插入,插入之后判断链表个数是否到达变成红黑树的阙值8;
               也可以遍历到有节点与插入元素的哈希值和内容相同,进行覆盖。
       5.如果桶满了大于阀值,则resize进行扩容
6、HashMap的扩容方式?负载因子是多少?为什是这么多?
      扩容方式:数组实际长度 > 数组容量长度 * 负载因子
      负载因子:0.75
7、默认初始化大小是多少?为啥是这么多?为啥大小都是2的幂?
       默认初始化容量:16
       为啥是16:因为在使用不是2的幂的数字的时候,Length-1的值是所有二进制位全为1111,这种情况下,index的结果等同于HashCode后几位的值。
       为啥是2的幂:为了数据的均匀分布,减少哈希碰撞;因为确定数组位置是用的位运算,若数据不是2的次幂则会增加哈希碰撞的次数和浪费数组空间。
                   当数组长度不为2的n次幂 的时候,hashCode 值与数组长度减一做与运算的时候,会出现重复的数据,
                   因为不为2的n次幂 的话,对应的二进制数肯定有一位为0 , 这样不管你的hashCode 值对应的该位,是0还是1,
                   最终得到的该位上的数肯定是0,这带来的问题就是HashMap上的数组元素分布不均匀,而数组上的某些位置,永远也用不到
       初始容量16,n只要是2的幂,n - 1的二进制一定全是1,假设有i位,那么它和任何hash做&操作肯定都是保留hash的后i位(&操作两个都是1才是1),
       这样就非常滴均匀,不容易冲突。            
                   https://blog.csdn.net/eaphyy/article/details/84386313
                   https://www.cnblogs.com/seakyfly/p/13391638.html
8、Java7和Java8的区别?
9、HashMap为啥会线程不安全?
   get、put方法没有加锁,有可能造成元素的覆盖
   以put方法为例:判断table的在(n-1)&hash的值是否为空,为空就新建一个节点插入在该位置:if ((p = tab[i = (n - 1) & hash]) == null)
   假如现在有两个线程都执行到了上图中的划线处。当线程一判断为空之后,CPU 时间片到了,被挂起。
   线程二也执行到此处判断为空,继续执行下一句,创建了一个新节点,插入到此下标位置。
   然后,线程一解挂,同样认为此下标的元素为空,因此也创建了一个新节点放在此下标处,因此造成了元素的覆盖。   
10、有什么线程安全的类代替么?
   1、HashTable:get、put方法加synchronize锁,简单读取时也加锁,而且锁住的是整张表,导致效率低下
   2、CurrentHashMap:
11、HashMap的为啥用尾插法? 
   1、1.7 用头插法:新来的值会取代原有值得位置,原有的值就顺推到链表中去,在扩容时,
          头插法会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题,
          单线程下正常,多线程下,会有可能形成链表成环(死循环),导致CPU爆满,程序崩溃
   2、1.8 用尾插法:主要是为了安全,防止环化(维护了链表元素的原有顺序,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题)

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档