1.基本原理?
jdk1.8 : 数组 + 链表/红黑树
hashmap的初始默认容量:16,如果new Hashmap的时候指定了,也必须是2的幂次方
hash冲突时:先使用链表解决冲突--->链表的长度到8,则使用红黑树
hash算法:先高16位异或低16位再取模运算 why?参考:https://blog.csdn.net/weixin_34288121/article/details/93446872
先高16位异或低16位再取模运算的目的:减少hash冲突
默认扩容阈值/负载因子:0.75f
2.如何put?
1).寻找slot
2).根据情况插入
2.1 slot为null,没有hash冲突,直接插入
2.2 slot为单节点:hash冲突,变成链表
2.3 slot为链表,hash冲突,在链表的尾部追加节点
2.4 slot为红黑树,将节点插入红黑树(涉及到红黑树的概念、原则、重新构建,左旋右旋等过程....)
3).hashmap容量不够,达到条件需要先扩容
3.hashmap什么时候扩容?
扩容是懒加载类型的:put的时候确定是否扩容,扩容时候直接生成一个容量为2倍的数组,然后进行数据迁移
影响发生Resize的因素有两个:
1)LoadFactor:HashMap负载因子,默认是0.75f。
当 HashMap.size >= Capacity*LoadFactor 时,HashMap可能会进行Resize。
2)当前数据存储的数量(即size())大小必须大于等于阈值(例如初始值16,负载因子0.75,阈值为12);
当前加入的数据是否发生了hash冲突。
因为上面这两个条件,所以存在下面这些情况
(1)就是hashmap在存值的时候(默认大小为16,负载因子0.75,阈值12),可能达到最后存满16个值的时候,再存入第17个值才会发生扩容现象,因为前16个值,每个值在底层数组中分别占据一个位置,并没有发生hash碰撞。
(2)当然也有可能存储更多值(超多16个值,最多可以存26个值)都还没有扩容。原理:前11个值全部hash碰撞,存到数组的同一个位置(这时元素个数小于阈值12,不会扩容),后面所有存入的15个值全部分散到数组剩下的15个位置(这时元素个数大于等于阈值,但是每次存入的元素并没有发生hash碰撞,所以不会扩容),前面11+15=26,所以在存入第27个值的时候才同时满足上面两个条件,这时候才会发生扩容现象。
4.如何扩容?
1)创建一个新的Entry空数组,长度是原来的2倍。
2)数据迁移:遍历原Entry数组,把所有的Entry重新Hash到新数组里。
为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变了。
让我们了解一下Hash公式:
jdk1.8 : index = HashCode(key) & (Length - 1)
jdk1.7: index = HashCode(key)% Length
当你经过运算的时候,你就会发现,两种方式得到的结果是一致的,所以都正确。
注意:jdk1.7版本下,HashMap并不是线性安全的,在并发的情况下可能会形成链表环。
static final int hash(Object key) { //jdk1.8 & jdk1.7
int h;
// h = key.hashCode() 为第一步 取hashCode值
// h ^ (h >>> 16) 为第二步 高位参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
5.扩容时如何数据迁移?
1.slot为null,表示没有数据,不需要操作
2.slot为单个节点,表示正常数据,直接拿到新数组相同下标的位置即可
3.slot为链表:拆分高低位链表,低位链表放到新数据的相同下标位置,高位链表放到新数组的 原slot位下标+原数组长度 位置
4.slot为红黑树:与链表相似,因为hashmap中的红黑树不只是红黑树,同时还维护了一个链表,用来数据迁移使用;同样拆分出高低位链表--------不同的是,如果高低位链表,长度大于6,则需要重新转化为红黑树