数组初始化:
1.在第一次放入数据时,会先初始化数组,默认长度16。
但是如果是2,4或者8会不会有点小,添加不了多少数据就会扩容,也就是会频繁扩容,这样岂不是影响性能,那为啥不是32或者更大,那不就浪费空间了嘛,所以啊,16就作为一个非常合适的经验值保留了下来。
2. 如果指定了参数初始化HashMap,会通过下面方法算出一个长度进行扩容,保证都是2的 整数次方。
扩容:
条件:大于容量*负载因子
3.当数组的如果没有超过1<<30最大长度,则进行扩容,长度为原来的两倍
扩容的时候为什么1.8 不用重新hash就可以直接定位原节点在新数据的位置呢?
put操作
1.如果数组为空,则进行初始化数组
2.如果数组不空,根据key算出hash值,然后取得数组的下标,查看该位置是否有值,如果没有则new一个node节点
3.如果有值,比较key是否相同,如果相同进行值覆盖
4.如果不相同,判断头结点是否是红黑树,如果是进行插入到红黑树
5.如果是链表,遍历链表,比较key是否相同,如果相同则值覆盖
6.如果遍历到链尾,数据插入到链尾,同时判断链表的长度>=8,如果是则进行链表转换成红黑树,
如果数组的长度<64,则进行扩容,否则建立红黑树
7.判断是否需要扩容( 元素数量>数组大小(默认16)* loadFactor(默认0.75)
)
为什么负载因子loadFactor是0.75?
0.75是时间跟空间上达到一个平衡的一个值。
为什么红黑树是转换条件是8?
1.在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,当长度为 8 的时候,概率仅为 0.00000006。这是一个小于千万分之一的概率,概率已经很低了。选择了8作为阀值(官网解释)
2.红黑树的平均查找长度是log(n),长度为8,查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
什么时候红黑树会退化成链表?
桶的链表还原阈值:即 红黑树转为链表的阈值,当在扩容(resize())时(此时HashMap的数据存储位置会重新计算),在重新计算存储位置后,当原有的红黑树内数量 < 6时,则将 红黑树转换成链表。主要是一个过渡,避免链表和红黑树之间频繁的转换。如果阈值是7的话,删除一个元素红黑树就必须退化为链表,增加一个元素就必须树化,来回不断的转换结构无疑会降低性能,所以阈值才不设置的那么临界。
HashMap 为什么选择红黑树
因为AVL树插入节点或者删除节点,整体的性能是不如红黑树的。AVL每个左右节点的高度是不能大于1的。所以维持这种结构比较消耗性能。主要还是左旋右旋改变与维护AVL高度问题,红黑树比他好的就是只需改变节点颜色就可以了。
二分查找树,他的左右节点不平衡,一开始就固定了root,那么极端的情况下会成为链表结构。
链表长度越长,那么他的插入和查询效率都很低。
而红黑树他的整体查找,增删节点的效率都是比较高的。
为什么扰动函数?
1.使用扰动函数就是为了增加随机性,让数据元素更加均衡的散列,减少碰撞。
2.算法一定要尽可能高效,因为这是高频操作, 因此采用位运算;
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
1.8还有三点主要的优化:
1.数组+链表改成了数组+链表或红黑树;将时间复杂度由O(n)
降为O(logn)
;
2.链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后;避免了在多线程环境下会1.7版本扩容会产生环
3.扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;
4.在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;
1.8在put值的时候,如果是覆盖原理的值,会直接返回,不需要扩容,如果先判断是否要扩容后插入的话比较浪费时间
面试题:
参考:
https://bugstack.blog.youkuaiyun.com/article/details/107903915
https://www.cnblogs.com/myseries/p/10876828.html
https://www.cnblogs.com/jzb-blog/p/6637823.html