前言
上一篇我们学习了List集合。下面我们继续学习map集合中的hashMap。我们思考下:hashMap中,底层结构是什么?添加元素的时候,是怎么put的?扩容的原理又是什么?JDK1.8为什么要引入红黑树,解决什么问题?
1、HashMap的底层结构
在JDK1.7的时候,HashMap底层是采用:
数组+链表(单向链表)的方式
头插法
JDK1.8的时候,HashMap底层是采用:
数组+链表(单向链表)+红黑树的方式,当链表的长度超过8的时候而且数组长度大于或等于64的时候,链表会转为红黑树,当红黑树的节点个数小于6的时候,重新转回链表
尾插法,解决1.7的扩容死循环问题
JDK1.7HashMap和JDK1.8HashMap的区别:
1.7中因为没有红黑树,链表容易过长。而且有个致命BUG,再多线程的情况下扩容,可能会出现死循环问题
1.8中,采用了红黑树来解决链表过长,查询效率低下问题。而且采用了尾插法解决了死循环问题
2、HashMap的初始化因子0.75和初始化容量
初始化容量大小为16。
初始化因子选择0.75。是因为这个是最佳选择。加载因子越大,index下标冲突概率也就越大、反而空间利用率是非常高的;
加载因子越小,index下标冲突概率也就越小,反而空间利用率不是非常高;
如果index下标冲突越高、反而查询的成本非常高,反之非常低
因此,必须在 “冲突的机会”与”空间利用率”之间寻找一种平衡与折衷
官方推荐使用0.75作为加载因子
3、HashMap的hash冲突问题
hash冲突:对象不同,但是hashCode相同,产生冲突后,会存放到链表中去,然后通过equal来比较相同的key,对象是否相同
index冲突:是因为底层做二进制运算产生相同的index,对象不同,但是index相同,源码中,计算index的时候,在&运算中把长度搞成奇数,来减少下标冲突问题,一旦有下标冲突,也是存放到链表中去(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)
3、扩容的原理
判断当前的size是不是大于阈值(容量 初始因子),而且在产生index冲突下才进行扩容,减少资源消耗,每次扩容2(为了再计算hash值和下标的时候减少hash冲突)的容量,先重新计算元素的index位置,防止取值的时候index错乱。再计算新增的容量的index值,进行扩容。
扩容的时候hash值不需要重新计算,只计算index值
4、JDK1.7中扩容死循环的问题,1.8中如何解决
再1.7中,死循环问题描述:在多线程的情况下,同时对hashMap进行扩容,扩容的时候会重新计算index下标,把原来的值移到新的index去,因为采用的是头插法,导致会改变next的引用顺序,又因为hashMap线程不安全,多线程情况下。另外一个线程的指向会反过来,会造成一直引用循环下去,造成死循环。
1.8使用尾插法,修复了多线程扩容死循环
5、源码流程分析
- 1、判断链表是否为空,为空的话则进行第一次扩容(初始化数组长度为16,阈值为12),用于第一次put值
- 2、计算数组的下标(hash值得计算(hashCode() 的高 16 位异或低 16 位,(h = k.hashCode()) ^ (h >>> 16))和index的计算,index的计算是tab[i = (n - 1) & hash]),判断这个数组是否为空,为空直接存入链表元素
- 3、不为空,则表示产生了index冲突或者hash冲突。如果hash值相同而且key相同,则进行值得覆盖
- 4、如果index链表类型是红黑树的话,按照红黑树的插入规则进行插入
- 5、如果index链表类型不是红黑树(还是链表),循环遍历,找到链表中最后一个元素,而且元素长度没有大于8,进行尾插法(测试下存放元素到哪里就转红黑树),长度大于或等于8而且数组的长度小于64的时候,会进行扩容,而且会先转双向链表,再到红黑树进行插入(TreeNode即是双向链表又是红黑树)
- 6、最后当数组长度大于阈值,则进行扩容,因为扩容完成后要进行下标的计算,resize()的时候会判断链表是不是红黑树,是的话重新计算index的时候会查看双向链表中的长度是不是小于6,是的话把红黑树转回单向链表
//put值的时候,大纲方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
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) {
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;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
- 为什么重写Equals还要重写HashCode方法
因为默认情况下,对对象间进行Equals,默认比较的是地址,这个时候会不相等,当重写了equals之后,对象相等,但是HashCode还是不等的,阿里巴巴手册规定要一起重写
- HashMap如何避免内存泄漏问题
重写Equals和重写HashCode方法
- HashMap1.7底层是如何实现的
数组加链表
- HashMapKey为null存放在什么位置
为null的时候存在数组第0个位置
- HashMap如何解决Hash冲突问题
采用链表加红黑树
- HashMap底层采用单链表还是双链表
单向链表
- 时间复杂度O(1)、O(N)、O(Logn)区别
时间复杂度为O(n) 从头查询到尾部,查询多次
时间复杂度为O(1) 查询一次 比如根据数组下标查询
时间复杂度为O(logn) 平方查询 比如红黑树
- HashMap根据key查询的时间复杂度
当没有发生Hash冲突的时候,则时间复杂度为O(1)
当发生Hash冲突,只有链表存放的时候,时间复杂度为O(n)。如果转为了红黑树,则时间复杂度为O(logn)
- HashMap底层是有序存放的吗?
无序的
10、LinkedHashMap 和 TreeMap底层如何实现有序的
采用双向链表来实现
11、HashMap底层如何降低Hash冲突概率
扩容的时候采用2的N次方,然后计算的时候,用当前的长度减去1,保证计算出来的值最后一位是1,这样后面算出来的index能减少hash冲突
12、HashMap中hash函数是怎么实现的?
13、为什么不直接将key作为哈希值而是与高16位做异或运算?
为了减少hash冲突
14、HashMap如何存放1万条key效率最高
15、HashMap高低位与取模运算有那些好处
增加速度,而且能搞减少hash冲突
16、HashMap1.8如何避免多线程扩容死循环问题
采用尾插法
17、为什么加载因子是0.75而不是1
是因为这个是最佳选择。加载因子越大,index下标冲突概率也就越大、反而空间利用率是非常高的;
加载因子越小,index下标冲突概率也就越小,反而空间利用率不是非常高;
如果index下标冲突越高、反而查询的成本非常高,反之非常低
因此,必须在 “冲突的机会”与”空间利用率”之间寻找一种平衡与折衷
官方推荐使用0.75作为加载因子
18、为什么HashMap1.8需要引入红黑树
为了解决链表过长的问题
19、为什么链表长度>8需要转红黑树?而不是6?
20、什么情况下,需要从红黑树转换成链表存放?
当链表的长度超过8的时候而且数组长度大于或等于64的时候
21、如何在高并发的情况下使用HashMap
22、ConcurrentHashMap底层实现的原理