HashMap

快速根据 key 找到 value 的“键值对集合” ,查询和插入平均时间复杂度都是o(1)

底层数据结构

1.7为数组+链表

1.8为数组+红黑树/链表

为什么要用红黑树,为何不一上来就树化?

链表太长,会影响查询性能,出现树化意味着被DDoS攻击了,即 被人恶意哈希冲突攻击(构造大量相同

的hashkey),树化可以缓解这种情况。

而平衡二叉树在极端情况下也是有这个问题,相当于链表了,红黑树比二叉树好一点,因为红黑树追求

大致平衡,自平衡效率高。

树化应该是一种偶然现象,链表短的情况下没必要,查询性能很高了,而且链表的占用内存要比树

要少

何时会树化,树化阈值为什么是8?

数组长度>=64 且链表长度>=8 就会进行扩容

jdk开发者他们基于大量的实验验证出来的,什么泊松分布,在负载因子为0.75的情况下,链表长度为8

的概率为0.00006,即选择8是让树化的概率最小

红黑树何时退化

- 扩容拆分树时,发现树的元素小于6,也会链表化
- remove树节点时,发现root,root.left,root.rigth,root.left.left为null(移除前检查),也会链表化

索引计算?为什么数组容量要为2的n次幂

idnex=hash&(size-1),相当于hash%size,但前提是size为2的n次幂

这个是为了底层能通过位运算来提高性能,还有一个就是扩容的时候如果index=oldCap&(size-1)==0,

说明还在原位,如果不是newIndex=oldIndex+oldSize。综上所叙,size为2的幂次方的好处挺多的

数组容量可以不为2的n次幂吗

可以的。size为2的n次幂虽然性能比较高,但如果hash全是偶数,运算出来的index就都是偶数,这

就导致hash分布不均

若追求更好的hash分布,可以用质数作为容量,这样还不用二次hash

例如hashTable它的扩容策略就是 oldCap*2+1

hashmap的容量如何设置?

hashCode都有了,为什么还要调用HashMap的hash()方法对hashCode进行二次哈希?为什么用异或?

jdk1.8中是拿着hashcode()的高16位和低16位进行异或

jdk1.7中是拿hashcode()多次移位,异或

用异或是为了扰乱hash,让hash分布更加均匀,只要32位中的一位不同,hash()返回的结果就不同

put方法流程?1.8和1.7的区别

1.HashMap为懒加载,即初始化只会初始化负载因子,而不是初始化容量

2.计算索引(桶下标)

3.判断索引位置是否为null,为null就创建Node节点插入

4.不为null,则判断是链表还是树,创建对应的节点走相应的添加or更新。如果是链表达到了树化的

逻辑,就走树化的逻辑(链表长度>=8且数组长度>=64)

5.添加后,判断是否需要扩容

1.7和1.8的扩容也是有所不同

1.7扩容时链表节点移动为头插入法,而1.8为尾插法

1.7是大于阈值且插入的桶没空位才会扩容(懒扩容),1.8是大于阈值就会扩容(主动扩容)

1.8扩容时计算Node的索引时,会有优化(位置不变+oldCap)。1.7是重新hash

get()方法流程

1.计算hash值

2.定位桶索引

3.通过hash()和equals方法和第一个桶节点比较是否相等,若相等就返回,不相等就进行链表or红黑树的

查询逻辑

加载因子为什么为0.75

在空间和时间查询性能之间能取得较好平衡

大了,链表长度容易长

小了,就经常扩容,影响性能

但其实根据二项分布,0.693是最好的选择,但考虑到size为2的n次幂,故取到了0.75,其实0.695….其

实也可以,只不过从中位数的角度,0.75是最好的

多线程下出现的问题

1.7的时候扩容时为头插入法,就会出现死循环

丢数据

两个线程同时判断一个桶位置可以插入位置,就插入了,然后就造成数据被覆盖了,而不会走解决

hash冲突的逻辑

1.7为什么要采用头插入法

首先HashMap本身就不是为多线程设计的,头插入法在线程安全的情况下完全是合适的,而且实现起来

也很简单

还有就是jdk开发者认为后插入的元素更加热点,放在头节点比较合适

jdk8为什么采用了尾插入法

1.jdk8引入的红黑树,那么可以顺便遍历一下链表的元素,统计个数,判断是否需要树化

2.避免多线程下扩容可能产生的死循环

key能否为null?key对象有什么要求?

HashMap的key可以为null。对象必须实现hashcode()和equals方法,hashcode()是定位,equals是比较

两者是否相等,并且key的内容必须不能被修改

hash冲突解决方法

拉链法:hash冲突时,同一槽位的拉成一条链表,hashmap采用的

开放寻址法:发生hash冲突时,寻找其他位置,这就保证了一个位置只有一个元素

线性探测:hash冲突时,往后找,直到找到第一个为null的位置,ThreadLocalMap采用的

二次探测:跟线性探测类似,只不过往后探测的步长是有规律的,2,4,8,16

二次hash