HashMap原理

HashMap

基本知识点
  • hahsMap 负载因为默认为0.75,作用是用于决定什么时候扩容、
  • 默认数组大小为16,并且数组大小永远为2的倍数,即使我们实例化时候传入非2的倍数,map内部也会找一个最接近的2的倍数大小替代它使用
  • 每次扩容都是2的倍数,因为数组下标是有key的hash和数组长度-1做与运算得到的。对于2的倍数-1的16进制,低位都是1,1&1=0,1&0=0,这样可以保证hash每一位都参与计算,使得更加散列。如果非2的倍数,出现了0,那么0和任何数与都是0,那么有些下标永远计算不到。
  • 当数组长度大于64时,且链表长度大于8,那么会转换链表为红黑树,当链表长度小于6时候,会再讲红黑树转为链表。转换只会在put元素时候触发
简介概括原理
  1. HashMap内部维护了一个Node对象数组,注意的是:实例化hashMap对象时候,并不会给内部维护的数组table初始化,仅仅是在hashMap对象中保存一下传入的默认数组大小和负载因子。数组第一次初始化是在put时候。
  2. 每次put值的时候,会先对Key进行hash散列运算,并和数组长度-1做与运算,得到其在数组中的下标。
  3. 首先判断数组是否已经初始化过,如果没初始化过,会调用resize()调整数组长度并进行初始化操作。
  4. 根据下标找到对应的Node对象,如果对象为Null,那么就实例化一个Node对象,保存key和value。如果对象不为Null,那么判断其对象是否是TreeNode,如果对象类型是TreeNode,那么说明当前存储的红黑树,如果不为TreeNode类型,那么说明存储的是链表式结构。
  5. 如果是红黑树,那么通过红黑树的排序性质(二叉排序树)来查找节点,如果找不到节点,则根据value大小在红黑树合适的位置来插入Node。插入结束,根据需要来平衡红黑树。
  6. 如果是链表,那么依次遍历比对key查找节点,查找到后替换值,没找到则在后续插入新对象Node。
  7. 插入结束后,会判断元素个数和数组长度乘负载因子的大小,从而决定是否扩容
  8. 获取元素时候,直接查找元素获取对象
  9. 移除元素时候,先查找到对应元素,然后再移除,移除时候并不会触发resize方法,从而也不会转换链表和红黑树结构。

HashMap原理.png

关于数组扩容,resize方法+tableSizeFor方法
  • capcity

    HahsMap中实际没有专门定义这个变量,只有一个默认值16,这个变量的含义是指map中数组的长度大小,是我们可以在实例化map时候传入的。我们实例化对象时候传入的数组长度,如果不是2的倍数,那么map中会找到最接近其的2的倍数作为数组长度。这里找到最接近其2的倍数使用的方法就是tableSizeFor。

    每次数组如果判断需要扩容时候,那么都会调用resize方法进行处理,这里包括数组的第一次初始化。

  • resize方法做的事情
  1. 将原数组长度乘2算出新的数组长度。
  2. 根据负载因子和当前数组长度计算出新的数组最大元素限制,保存到map中。
  3. 生成新的Node数组
  4. 遍历当前的table,将每个元素都放到新数组中的对应位置中。
  • 什么时候会触发扩容调用resize方法呢?

    从源码流程中可以看到,在put元素时候,如果元素个数大于数组长度乘以负载因子,那么就会触发扩容

  • 什么时候红黑树转为链表呢?

    在每次扩容方法resize被调用后,在扩容方法里会判断如果是红黑树,那么会调用其spilt方法判断是否Node个数小于等于6,如果是则转为链表结构。

所以在删除元素时候并不会触发红黑树转为链表

根据key查找下标的方法
  1. 调用key的hashCode方法,得到hash值
  2. 取hash的高16位和其低16位做异或得到新的hash值作为key的最终hash值,记为hash1
  3. 拿hash1和数组长度-1做与,得到数组下标。
  • 高16和地16做异或原因

数组长度一般不会特别大,将hash高16和低16做一次异或运算,保证key的hash全部参与数组下标的计算,增加散列度。

  • 和数组长度-1做与原因

防止下标越界。

hashMap key可以为Null吗

可以,对于key为null,那么会将数组保存到数组的第一位,即数组下标永远为0

为什么HashMap的默认负载因子是0.75呢?而不是别的呢?

设计者通过各种计算得出的最优扩容。

jdk1.7和jdk1.8 HashMap 的区别
  1. jdk1.7不存在红黑树,1.8存在红黑树,1.7结构是数组+链表,1.8是数组+链表+红黑树
  2. 1.7将数据插入到链表头部,1.8插入数据到链表尾部
  3. 1.7在元素插入前检查扩容,1.8是在插入后检查扩容
为什么HashMap线程不安全
  • 对于jdk1.8

    我们插入数据时候,如果数组下标当前是null,那么会直接生成一个新的Node对象插入数组,多线程中,这里可能出现同时插入一个新数据,后边数据将前边数据覆盖的情况。

  • 对于jdk1.7

    1.7链表中插入Node是插入到头部,所以每次扩容后,原链表相当于倒置的插入到了新的数组中。他会从旧数组的链表头部开始遍历,查找其在新数组的下标,然后插入,如果存在A和B,在旧数组中链表位置为A->B,扩容后,他两正好还在同一个数组下标,那么有以下流程。

多线程扩容时候:

  1. 线程1,扩容时候会循环遍历链表,首先拿出A,赋给临时遍历E,
  2. 此时线程2已经执行完扩容,数组现状是对应下标为B->A
  3. 接着线程1继续执行,将E的next指向新数组下标的元素,此时新数组下标的元素已经是B了,所以A->B->A,接着,死循环就开始了。
putMapEntries方法什么用,做什么事

对于我们实例化时候传入的map集合,该方法用于遍历传入的map,将其内部数据依次传入到当前的map中。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYqEolFe' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片