HashMap相关的一系列问题
1.底层实现
1.7 数组 链表
1.8 数组 链表 红黑树
当集合要添加新的元素时,先调用这个元素的hashCode方法,如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。所以这里存在一个冲突解决的问题。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。
1、相同的对象必须具有相等的哈希码(或者散列码)。
2、如果两个对象的hashCode相同,它们并不一定相同。
存取:两次hash–> 用capacity取余–> 得出桶下标
扩容后是会计算桶下标的 可以解决链表过长
桶容量大于
2.为什么用红黑树
原来的链表那种查找太慢了
3.什么时候会扩容
Hashmap的扩容需要满足两个条件:
当前数据存储的数量(即size())大小必须大于等于阈值;(默认大小为16,负载因子0.75,阈值12
当前加入的数据是否发生了hash冲突。(17会判断这个,18只要超过阈值就会扩容,不管冲突没)
4.什么时候会树化
- 长度超过树化阈值8
- 整个数组长度>= 64 (小于64会先扩容,来解决链表过长)
5.树化阈值为什么是8
在受到攻击和用户实现这种不好的hash算法 时可以保证一定的性能。 尽量使用链表
hash值如果足够随机,则在hash表内按泊松分布,在负载因子0.75的情况下,长度超过8的链表出现概率是0.00000006,选择8就是为了让树化几率足够小。
源码中的解释:
当hashCode离散性很好的时候,树型bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值。但是在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。不过理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循泊松分布,我们可以看到,一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。
5.什么时候会退化成链表
-
扩容后 导致树拆分 长度<=6
-
remove树节点之前
若root、, root.left、 root.right、 root.left.left有一个为null,也会退化为链表
6.为什么不直接用树
- 短的时候 性能相似
- 树结点保存的数据多 占空间
7.索引(桶下标)是怎么计算的
两次hash–> 用capacity取余–> 得出桶下标
取余的优化:
初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
因为size一定为2的n次幂 所以说可以使用index = hash & (tab.length – 1)来取余做计算
容量是2的次幂的时候 性能较好,容量为质数时 hash分布较好(.net)。hashmap选择了2的次幂。二次hash也是保证分布性
8.sHashmap的put流程
插入链表: JDK1.7版本及以前使用是头插法;JDK1.8使用的是尾插法
9.为什么1.8改成尾插了
扩容时迁移 多线程下 一个线程已经迁移 另一个也迁移时 发生
ps:多线程下hashmap本来就不安全
10.为什么负载因子是0.75f
11.hashcode里不能是可变对象
如果K是一个对象,hashcode中用到了对象属性,改变对象属性后,原来的K就失效了。