HashMap 特点:
存储无序的。
键和值位置都可以是 null,但是键位置只能存在一个 null。
键位置是唯一的,是底层的数据结构控制的。
jdk1.8 前数据结构是链表+数组,jdk1.8 之后是链表+数组+红黑树。
阈值(边界值)> 8 并且数组长度大于 64,才将链表转换为红黑树,变为红黑树的目的是为了高效的查询。
流程图:
size 表示 HashMap 中键值对的实时数量,注意这个不等于数组的长度。
threshold(临界值)= capacity(容量)* loadFactor(负载因子)。这个值是当前已占用数组长度的最大值。size超过这个值就重新 resize(扩容),扩容后的 HashMap 容量是之前容量的两倍。
面试题
HashMap 中 hash 函数是怎么实现的?还有哪些hash函数的实现方式?
答:对于 key 的 hashCode 做 hash 操作,无符号右移 16 (>>>>)位然后做异或运算(^),按位与(&)计算出索引。还有平方取中法,伪随机数法和取余数法。这三种效率都比较低。而无符号右移 16 位异或运算效率是最高的。
当两个对象的 hashCode 相等时会怎么样?
答:会产生哈希碰撞。若 key 值内容相同则替换旧的 value,不然连接到链表后面,链表长度超过阈值 8 且数组长度超过64就转换为红黑树存储。
什么是哈希碰撞,如何解决哈希碰撞?
答:只要两个元素的 key 计算的哈希码值相同就会发生哈希碰撞。jdk8 之前使用链表解决哈希碰撞。jdk8之后使用链表 + 红黑树解决哈希碰撞。
如果两个键的 hashCode 相同,如何存储键值对?
答:通过 equals 比较内容是否相同。相同:则新的 value 覆盖之前的 value。不相同:则将新的键值对添加到哈希表中。
问题:为什么 Map 桶中结点个数超过 8 才转为红黑树?
我们都知道,链表的时间复杂度是O(n),红黑树的时间复杂度O(logn),很显然,红黑树的复杂度是优于链表的,既然这么棒,那为什么hashmap为什么不直接就用红黑树呢?请看下图
源码中的注释写的很清楚,因为树节点所占空间是普通节点的两倍,所以只有当节点足够多的时候,才会使用树节点。也就是说,节点少的时候,尽管时间复杂度上,红黑树比链表好一点,但是红黑树所占空间比较大,综合考虑,认为只能在节点太多的时候,红黑树占空间大这一劣势不太明显的时候,才会舍弃链表,使用红黑树。说白了就是空间和时间的权衡。
为了配合使用分布良好的hashCode,树节点很少使用。并且在理想状态下,受随机分布的hashCode影响,链表中的节点遵循泊松分布,而且根据统计,链表中节点数是8的概率已经接近千分之一,而且此时链表的性能已经很差了。所以在这种比较罕见和极端的情况下,才会把链表转变为红黑树。因为链表转换为红黑树也是需要消耗性能的,特殊情况特殊处理,为了挽回性能,权衡之下,才使用红黑树,提高性能。也就是大部分情况下,hashmap还是使用的链表,如果是理想的均匀分布,节点数不到8,hashmap就自动扩容了。
综上所述,我们可以看到,一个 桶 中链表长度达到 8 个元素的槪率为 0.00000006,几乎是不可能事件。所以,之所以选择 8,不是随便決定的,而是裉据概率统计决定的。甶此可见,发展将近30年的 Java 每一项改动和优化都是非常严谨和科学的。
也就是说:选择 8 因为符合泊松分布,超过 8 的时候,概率已经非常小了,所以我们选择 8 这个数宇。
补充:
Poisson 分布(泊松分布),是一种统计与概率学里常见到的离散[概率分布]。泊松分布的概率函数为:
泊松分布的参数 A 是单位时间(或单位面积)内随机事件的平均发生次数。泊松分布适合于描述单位时间内随机事件发生的次数。
以下是在一些资料上面翻看的解释,供大家参考:
红黑树的平均查找长度是 log(n),如果长度为 8,平均查找长度为 log(8) = 3,链表的平均查找长度为 n/2,当长度为 8 时,平均查找长虔为 8/2 = 4,这才有转换成树的必要;链表长度如果是小于等于 6, 6/2 = 3,而 log(6) = 2.6,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
增加方法 put() (重点)
put方法是比较复杂的,实现步骤大致如下:
1.先通过 hash 值计算出 key 映射到哪个桶;
2.如果桶上没有碰撞冲突,则直接插入;
3.如果出现碰撞冲突了,则需要处理冲突:
4.a 如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;
5.b 否则采用传统的链式方法插入。如果链的长度达到临界值,则把链转变为红黑树;
6.如果桶中存在重复的键,则为该键替换新值 value;
7.如果 size 大于阈值 threshold,则进行扩容;