JDK1.7中的HashMap
JDK1.7中的HashMap是一个数据与链表的结合体。底层是一个数组结构,数组中的每一项都是一个链表,当新建一个HashMap的时候,就会初始化一个数组。
结构大致如下
hashmap的成员变量
1.DEFAULT_INITIAL_CAPACITY = 1 << 4;:初始桶(数组)大小为16,因为底层是数组,所以也是数组大小
2.MAXIMUM_CAPACITY = 1 << 30:桶最大值,2的30次方
3.DEFAULT_LOAD_FACTOR = 0.75f;默认的负载因子0.75
4.table:真正存放数据的数组
5. size:map中存放的键值对的数量。
6. threshold:resize扩容时的阈值
7. loadFactor:负载因子,可在初始化时显式指定。
在不设置初始化map大小的时候,默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,扩容是成倍的扩容,乘以就是32,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
在1.7中添加元素的put过程如下
- 1、先判断key是否为null,如果是null则会插入头结点table[0]的位置
- 2、key不为null,计算key的hashCode,对数组的大小取模,hash & (length-1),得到在数组中的位置,如果数组上已经有元素了,那么遍历该位置上的元素,如果存在hash、key都相等,那么说明是同一个key,则将原来的值覆盖,并且返回原来的值oldValue。如果这个元素上找不到要添加的key,说明是一个新值,则使用头插法,插入元素链表的头部
- 3、如果数组上没有元素,当前位置没有数据插入,那么会新增一个entry写入当前位置。
- 当调用addentry的时候,就会判断是否需要扩容,如果需要扩容,就重新计算hashcode,同时也需要重新定位元素的位置,扩容的时候会将旧数组中的元素迁移到新数组中
get 方法
- 先判断key的hashcode,定位到在数组的具体位置中
- 判断该位置是否是链表,如果是就遍历链表,直到找到key和hashcode相等的。不是链表就直接 key、key 的 hashcode 是否相等来返回值。啥都没有就返回空…
JDK1.7中并发场景下出现死循环
多线程同时put的时候,如果同时调用了resize操作,可能会导致循环链表的产生,进而使得后面get的时候,会死循环,数组扩容的时候将调用transfer函数将旧数组中的元素迁移到新的数组,这里会重新计算hashcode,使用是头插法方式。
假设有两个线程T1,和T2,HashMap的容量为2,T1线程存入key A B C D E 。T1线程中计算A B C的hash值相同,于是形成一个链接,假设 A->C->B,而D,E的hash值不同,于是容量不足,需要扩容把老的hash表中的数组迁移到新的hash表中(refresh)。这时T2线程进来,T1暂时挂起,T2也准备放入新的Key,这时也发现容量不足,需要扩容,也refresh一下,假设原来的链表结构为C->A,之后T1继续执行,链表结构为A->C,这时就形成了A.next=B,B.next=A的环形链表,一旦取值进入就会陷入死循环
JDK1.8中的HashMap
数据结构
JDK1.8中的HashMap底层使用的是数组+链表,链表过长会转为红黑树,链表长度超过8转为红黑树,低于6转为链表。
1.8中put方法:
1.对key求哈希值然后计算下标
2.如果没有哈希碰撞直接放入槽中(没有哈希碰撞说明当前位置没有内容)
3.如果发生碰撞了就以链表的形式放到后面
4.如果链表长度超过阈值,就把链表转为红黑树
5.如果节点已经存在就替换旧值
6.如果槽满了就需要扩容
1.8扩容过程
1.根据新的容量(2倍)新建一个数组,保存旧的数组
2.遍历旧数组的每一个数据,重新计算每个数据在新数组中的存储位置。(要么是原位置,要么是原位置+旧容量),将旧数组中的每个数据转移到新的数组中,使用尾插法。
1.8扩容和1.7扩容的区别
- 1.转移数组方式不同,1.7中是头插法,1.8中是尾插法
- 2.位置的计算方式不同,1.8中容量扩充为原来的2倍,扩容后的位置要么等于原位置,要么等于原位置+旧容量。1.7中是全部按照原来方法计算,全部重新计算一次hashcode
- 新数据的插入时机不同,1.8中是扩容前插入,转移数据时统一计算插入位置。1.7中的新数据是扩容后插入,插入位置也是转移老数据之后,再单独计算的。
JDK7是每拿到一个Node就直接插入到newTable,而JDK8是先插入到高低链表中,然后再一次性插入到newTable
哈希冲突有哪些解决方法?
1.开放地址法(开辟一个新的空间地址存储),
2,链地址法(在已有的链表上新增一个节点存储)。
1.8中发生了哈希冲突,就是使用了链接地址法+尾插法+红黑树 1.7中使用了链接地址法+头插法
为什么在JDK1.8中进行对HashMap优化的时候,把链表转化为红黑树的阈值是8,而不是7或者不是20呢?
节点分布在hash桶中的频率遵循泊松分布,桶的长度超过8的概率非常小,所以坐着根据概率统计选择了8,链表小于等于6树还原转为链表,大于等于8转为树,中间有个差值7可以有效防止链表和树频繁转换。
总结
1.数据结构
Java 7及以前是数组+链表、Java 8及以后是数组+链表+红黑树。
1.8中,当链表长度到达8的时候,会转化成红黑树。
2.插入链表的方式
1.7是头插法,1.8是尾插法
3.扩容时,调整数据的方式
1.7其实就是根据hash值重新计算索引位置,然后将数据重新放到应该在的位置。
1.8中按扩容后的位置要么等于原位置,要么等于原位置+旧容量
4.hash值的计算方式不同
jdk1.7获取hash值是9次扰动处理=4次位运算+5次异或,而JDK1.8只用了2次扰动处理=1次位运算+1次异或。
jdk1.8获取hash值,是将高16位和低16位进行异或就得到了。
5.新数据的插入时机不同
1.8中是扩容前插入,转移数据时统一计算插入位置。1.7中的新数据是扩容后插入,插入位置也是转移老数据之后,再单独计算的。
参考文章:
https://blog.csdn.net/qq_36520235/article/details/82417949
https://baijiahao.baidu.com/s?id=1675991555833901875&wfr=spider&for=pc
https://www.cnblogs.com/xiaosisong/p/12290251.html
为什么HashMap具备下述特点:键-值(key-value)都允许为空、线程不安全、不保证有序、存储位置随时间变化
为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键
String Integer等包装类,内部已经重写了hashcode和equals方法,保证了Hash值的不可更改性,计算准确性,而且又是final类型,保证了key的不可更改性
HashMap 中的 key若 Object类型, 则需实现哪些方法?
需要实现hashcode和equals方法
hashcode,计算需要存储数据的位置
equals:判断存储位置上是否已经存在当前Key,保证key在hash表中的唯一性