HashMap的扩容机制以及源码解释

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;
    }

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值