HashMap的扩容机制与源码。
本篇属于个人总结,
参考文档:https://blog.csdn.net/String0715/article/details/121802209
参考文档:https://blog.csdn.net/qq_44750696/article/details/125264681
其实每次都觉得自己看的差不多了,然后面试完,说自己研究的不够深。看了那么多视频和博客,居然还是不够,那么只能上源码了。
源码文档中很长,主要提到有TreeNode与Bin,树节点与箱子。文档中将存数据的数组称为箱子Bin。
初步的一些方法
有个静态方法Hash,看到有个右移16位的操作,然后再异或运算。
(h = key.hashCode()) ^ (h >>> 16)
将h的高位右移成低位,并于高位进行混合,将高位数据于低位数据进行特征混合,据说是为了后需调用该方法,于槽位值进行hash比较,是哪个桶的成员时,不丢失特征,专门这样做的。这样能减少hash冲突。
这里并不是必要的,几率并不大,但是这是谷歌工程师,精益求精的体现。
负载因子表示的是一个散列表空间的使用程度,负载因子越大链表也就越大,链表越大,索引效率也就会大大降低 有一个公式 initailCapacity*loadFactor=HashMap的容量。啥意思呢?要先知道,HashMap的宗旨是综合数组与链表,即加快搜索,又通过链表的方式,融入增删改的功能。那么若是链表过长,那就意味着查找速率变慢,也就是说希望链表的长度是数组长度的0.75倍。当然数组长度达到64,那就没办法了。
关键字 transient 于序列化有关,当类进行序列化写入时,可以看到有这个关键字的那个参数值,为空,也就是没有进行序列化。这就是这关键字的作用,序列化后节省空间?
然后是初始化方法。有参与无参方法。
public HashMap(int initialCapacity, float loadFactor)
初始化要传入最初的大小,以及加载因子。初始大小我认了,加载因子是个啥,不是固定的0.75吗。
初始化还是要经过判断, if (initialCapacity > MAXIMUM_CAPACITY) 这里是1<<30,这是总的大小。
对加载因子的判断就是它是否大于0,以及是个常数了。
然后,对initialCapacity还有一步操作:
this.threshold = tableSizeFor(initialCapacity);
tableSizeFor方法
tableSizeFor方法,目的是将传进来的参数转变为2的n次方的数值。能算出来稍大于传入的参数的,最大的二次幂-1,最后返回时加一,就正好是二次幂了。算法好说,为何要先减一是其他博客没有提的,我猜测应该是考虑边界值,假设传入为8,不减一,那么算出来会是15,再加一就是16,凭空多出一级。而先减一就能避免这种情况。这个推一步的想法,精妙。
还有普通带参的,加载因子直接就是0.75.,调上面的构造方法(this)。所以,才会有设置的参数都无效,会变成是二次幂的说法,就是上面的这个方法tableSizeFor。
无参的构造,只给全局变量赋值 this.loadFactor = DEFAULT_LOAD_FACTOR;
还有最后一个,直接传入一个Map类型的集合。
看到,加载因子还是0.75。然后是put方法。putMapEntries,内部借助负载参数,算出个ft值,也就是扩容后的大小(实际大小而不是比值,其实很奇怪为何算出来还要+1?),直接算扩容?然后再根据这个大小算二次幂,获得最终容量大小,还是回到了二次幂,这是是当table为0的情况,提前扩容。
若table已经有了,会比较合入的容量与,若不合适,则resize,resize方法是重要方法,后面再说。
然后把集合中元素一个个添加进去。 m.entrySet(),这个方法,非常的妙,它内部进行替换的思想,体现了对任何操作都是局部变量的操作,对全局的操作都是读。
然后遍历,将数据给存入现有的表中putVal(hash(key), key, value, false, evict);
其中还有两个参数evict与false。 evict if false, the table is in creation mode.
扩容的resize方法
resize方法 文档:
Initializes or doubles table size. If null, allocates in accord with initial capacity target held in field threshold. Otherwise, because we are using power-of-two expansion, the elements from each bin must either stay at same index, or move with a power of two offset in the new table.
初始化或加倍表的大小。如果为空,则根据字段阈值中保持的初始容量目标进行分配。否则,由于我们使用的是两个幂展开,每个箱子中的元素要么保持相同的索引,要么在新表中以两个偏移量的幂移动。
这个地方就是常问的扩容的地方了。其中有个变量int threshold,这个是个全局变量,在很多地方都用到了,文档中:The next size value at which to resize (capacity * load factor).初始容量为0,或16。不是很明白这个参数的作用。下一次扩容的阈值大小,提前计算的吗?
回到resize方法内,
int oldCap = (oldTab == null) ? 0 : oldTab.length; table是数组的长度,可是后续它跟2的30次幂比较。嗯,跟想的不一样。这样是全部节点的长度,那就意味着map中还保存着当前所有节点,并将其作为数组保存。—但是这是不可能的。
resize方法 扩容方法,有多处使用,其中64那个参数是转树时,有一层判断,数组不够64,还是选择扩容,否则转树。只有这一处有64位记录。转红黑树的限制时8、6。这个位置的话好像是在put的时候。
putVal方法
存值的方法大部分都是这里了。
根据谷歌的代码方式,这个地方也是考虑了很多种情况,我们一步步来。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i; // 不知道是又什么代码规范,很多地方都使用这种方式,将全局变量赋给本地变量来操作,是为了生命周期吗?
if ((tab = table) == null || (n = tab.length) == 0) // 这种情况对应数组为空的
n = (tab = resize()).length; // 局部变量n有了值,
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); // new个新节点,放入数组桶中,是第一个元素的情况。
else { // 那就是不为空的情况,需要插入的那种。
Node<K,V> e; K k;
// 不理解p的值哪里来的,毕竟没有赋值。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; // 若哈希值对应,则意味着相同节点,就是p点了。
// 这是只是把节点给了e,但是没有使用,另外p的值哪里来的。
else if (p instanceof TreeNode)
// 树节点的情况
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 否则++,轮询,放到尾部。若此时达到了8,则要判断一下是否转树。内部有个条件是64,桶长度小于64,则扩容而不是转树。否则转树。实际上这就是扩容机制了。
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { // 这就是搞p、e的原因吗?
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 上面的是换节点。也就是相同的hash值的情况,意味着要替换。注意break。
p = e;
}
}
查到某个节点,节点不为空,则说明需要替换。返回oldValue是谷歌源码中经常性的操作。这种的不需要扩容,所以直接return
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 若最后发现树太大了,再扩容一次。
if (++size > threshold)
resize();
afterNodeInsertion(evict); //尾插。
return null;
}
,