对于HashMap你知道多少?据说这里有源码分析

从一个问题引发的探究过程。HashMap底层实现的数据结构是什么?
---- 没错,数组+链表
为什么要用到这种结构?什么时候会发生从数组到链表的转换?扩容机制呢?HashMap会根据Key值计算出在数组上的位置,但如果Key为null的时候,怎么处理?

这么多内容,笔者自觉一篇文章也讲不清楚,接下来以数篇文章来进行讲解。

1.数组+链表的源码解读(基于JDK1.7,1.8的内容后期再写);
2.put方法做了些什么(根据Key计算键值对在数组中的位置;扩容;发生Hash冲突时进行链表转换);

首先放上一张图,形象地说明一下数组+链表的结构:
在这里插入图片描述

主要变量介绍

在这里插入图片描述

(1)transient int size :记录HashMap中key-value的个数;
(2)final float loadFactor :加载因子,默认0.75f;
(3)int threshold:临界值,等于capacity(容量) * load factor;

HashMap初始容量大小:16;

/**
     * The default initial capacity - MUST be a power of two.
     * 默认的初始容量大小
     */
static final int DEFAULT_INITIAL_CAPACITY = 16;

HashMap具有扩容机制,默认情况下,当size =threshold =capacity(16) * loadFactory(0.75) = 12 ,即达到扩容条件(HashMap中存储了12个key-value),数组的大小(size)会从16变成32。

下一次扩容时会变成64,依次类推16、32、64、128 … …

现在临界值(threshold) 、容量(capacity) 、 加载因子(loadFactory )的作用已经很明了了。
总结一下:HashMap的容量一旦达到临界值,就会执行扩容操作,而临界值根据容量加载因子计算得出。HashMap中存储的key-value数量达到容量的75%,即满足扩容条件。

也就是说,HashMap并不是存储到16的时候才进行扩容。

关于初始容量的设置:设置初始容量,必须是2的幂;就算给定的值不是2的幂,HashMap也会计算出大于指定容量最接近的2的幂作为初始容量;比如说设置初始容量为3,则打印出容量结果为4;
由此可见,HashMap还是很任性的,有自己的脾气;

put操作源码探秘

好了,解释了相关变量和概念,现在来看看put操作究竟做了什么。首先贴出源码。

public V put(K key, V value) {
        if (key == null)// 第一步
            return putForNullKey(value);
        int hash = hash(key); //第二步
        int i = indexFor(hash, table.length); //第三步
        for (Entry<K,V> e = table[i]; e != null; e = e.next) { //第四步
            Object k;
            //第五步
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i); //第六步
        return null;
}
  • 第一步:putForNullKey() --> 如果key为null,将key-value放入数组的第一个位置

  • 第二步:hash() --> 对key做hash运算,返回一个int类型的值

  • 第三步:indexFor() --> 用hash()得到的int值,对数组长度进行取模,计算出key在数组上的位置i。此处jdk源码采用位运算替代取模运算,提高计算效率。

  • 第四步:for (Entry<K,V> e = table[i]; e != null; e = e.next) --> 判断数组第i个元素是否为空,如果i不为空,则遍历i位置挂载的单向链表。

  • 第五步:for循环内部, if (e.hash == hash && ((k = e.key) == key || key.equals(k))) --> 判断此次插入的key的hash值和已存在元素的hash值是否相同(是否存在hash冲突),若二者hash值相同,且key值相等,则用新值覆盖原来的值。(简单理解为put了相同的key,替换掉value)。

  • 第六步:新建一个entry节点,将其放在数组i的位置上,作为链表的表头

此处详解addEntry()方法

void addEntry(int hash, K key, V value, int bucketIndex) {
 		//判断是否需要扩容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);//执行扩容操作
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

1.if逻辑判断是否需要扩容,当key-value的个数达到临界值,且当前数组i位置有值时,执行扩容操作
2.新建一个Entry

扩容操作-resize():耗时操作,二倍扩容,扩容完成后, 重新计算当前key在数组上的位置;

扩容操作过程:

创建一个新的数组,大小是原来的2倍。

  • 遍历原数组,将其存储的key值重新hash后,调用indexFor()方法重新计算其在新数组上的位置。
  • 将key-value存放到新数组上

具体操作详见代码,数组复制过程:

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

关于扩容的思考

为什么要有扩容操作?数组+链表不是已经能够解决掉hash冲突了吗?理论上再多的值都能够存储,数组似乎完全没有必要扩容吧?

  • 是的,存储肯定是能存储的,但随着put操作增加,HashMap中的key-value数量不断增多,此时每次调用get方法时,都需要在长长的链表上进行遍历,HashMap的get效率大大降低。
    所以扩容操作是有必要的。

~~

  • ps:分享over,如有错误之处欢迎在评论区指出。

~~

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值