7000 字说清楚 HashMap,面试点都在里面了 (4)

首次初始化分为无参初始化和有参初始化两种情况,前面在讲 HashMap初始化的时候说了,无参情况默认就是 16,也就是 table 的长度为 16。有参初始化的时候,首先使用 tableSizeFor()方法确定实际容量,最后 new 一个 Node 数组出来。

HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];

其中 newCap就是容量,默认16或者自定义的。

而这个过程中还有很重要的一步,就是维护扩容阈值。

扩容

put方法中,判断当 size(实际键值对个数)到达 threshold (阈值)时,触发扩容操作。

// 当实际长度大于 threshold 时 resize if (++size > threshold) resize();

HashMap遵循两倍扩容规则,每次扩容之后的大小是扩容前的两倍。另外,说到底,底层的存储还是一个数组,Java 中没有真正的动态数组这一说,数组初始化的时候是多大,那它就一直是这么大,那扩容是怎么来的呢,答案就是创建一个新数组,然后将老数组的数据拷贝过去。

拷贝的时候可能会有如下几种情况:

如果节点 next 属性为空,说明这是一个最正常的节点,不是桶内链表,也不是红黑树,这样的节点会重新计算索引位置,然后插入。

如果是一颗红黑树,则使用 split方法处理,原理就是将红黑树拆分成两个 TreeNode 链表,然后判断每个链表的长度是否小于等于 6,如果是就将 TreeNode 转换成桶内链表,否则再转换成红黑树。

如果是桶内链表,则将链表拷贝到新数组,保证链表的顺序不变。

确定插入点

当我们调用 put方法时,第一步是对 key 进行 hash 计算,计算这个值是为了之后寻找落点,也就是究竟要插入到 table 数组的哪个桶中。

hash 算法是这样的,拿到 key 的 hashCode,将 hashCode 做一次16位右位移,然后将右移的结果和 hashCode 做异或运算,这段代码叫做「扰动函数」,之所以不直接拿 hashCode 是为了增加随机性,减少哈希碰撞次数。

/** * 用来计算 key 的 hash 值 **/ static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }

拿到这个 hash 值之后,会进行这样的运算 i = (n - 1) & hash,其中 i就是最终计算出来的索引位置。

有两个场景用到了这个索引计算公式,第一个场景就是 put方法插入键值对的时候。第二个场景是在 resize 扩容的时候,new 出来新数组之后,将已经存在的节点移动到新数组的时候,如果节点不是链表,也不是红黑树,而是一个普通的 Node 节点,会重新计算,找到在新数组中的索引位置。

接着看图,还是图说的清楚。

HashMap 要求容量必须是 2 的 n 次方,2的 n 次方的二进制表示大家肯定都很清楚,2的6次方,就是从右向左 6 个 0,然后第 7 位是 1,下图展示了 2 的 6 次方的二进制表示。

image-20200615181108891

然后这个 n-1的操作就厉害了,减一之后,后面之前二进制表示中 1 后面的 0 全都变成了 1,1 所在的位变为 0。比如 64-1 变为 63,其二进制表示是下面这样的。

image-20200615181859017

下图中,前面 4 行分别列出了当 map 的容量为 8、16、32、64的时候,假设容量为 n,则对应的 n-1 的二进制表示是下面这样的,尾部一片红,都是 1 ,能预感到将要有什么骚操作。

没错,将这样的二进制表示代入这个公式 (n - 1) & hash中,最终就能确定待插入的索引位了。接着看图最下面的三行,演示了假设当前 HashMap的容量为 64 ,而待插入的一个 key 经过 hash 计算后得到的结果是 99 时,代入公式计算 index 的值,也就是 (64-1)& 99,最终的计算结果是 35,也就是这个 key 会落到 table[35] 这个位置。

为什么 HashMap一定要保证容量是 2 的幂次方呢,通过二进制表示可以看出,如果有多位是 1 ,那与 hash 值进行与运算的时候,更能保证最后散列的结果均匀,这样很大程度上由 hash 的值来决定。

image-20200615175605039

如何确保 key 的唯一性

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zypgjp.html