Java集合源码系列之HashMap添加元素的流程

前言

哈希表是一个很重要的数据结构,HashMap是java对这种数据结构的具体实现类。无论是做算法题还是项目中,HashMap的身影可以说 是随处可见。同时,面试中关于HashMap的考点也是非常高频。接下来,就跟笔者一起通过源码的方式,深入了解一下HashMap 添加元素 以及扩容的流程!如果大家对哈希表有所了解,建议直接跳到添加元素部分~~

本文是以jdk1.8中的HashMap作为基点的


哈希表

简介

首先什么是 哈希表,哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表,大家看到这两个名称知道都是指hash table就可以了)。

哈希表是根据关键码的值而直接进行访问的数据结构。

下面笔者就用一道算法题带大家了解一下哈希表以及哈希表的原理

给出5个0~9范围内的数字,再给出若干个目标数字target,要求判断给出的这5个数字中是否包含target。

输入:4 8 2 9 6

target:6

这道题最直接的写法是使用一个for循环,依次将每个数字与目标值target比较,如果相同则表示这10个数字中包含target。不过这样做有一个弊端,当题目条件改为给出10万个元素的时候,通过for循环的方法遍历每一个元素是不是性能就有点低了呢?

如果你不觉得低,那 改成10亿个元素呢?是不是此时用for循环一个个遍历就有点儿低效了。那么有没有更好的方法呢?答案是有的,但是这个更好是从性能的角度考虑的。

我们可以使用一个数组来记录某个数字是否出现过,数组的索引就代表对应的数字

在这里插入图片描述

如图中所示,使用一个长度为10的bool数组arr,把输入的数字转换为数组索引,将该索引上的值设置为true(bool数组默认初始化都为false),等输入完数字后,我们只需要判断arr[6]是否为true就能直接判断这堆数字里是否包含target,怎么样,是不是简洁又高效!!

其实数组就是最简单的哈希表,索引就是key,索引位置上的值就是value。先简单介绍一下key-value形式。看图大家应该也可以想到,哈希表其实就是以空间换时间的思想

哈希函数

下面,笔者将题的条件改一改

给出5个0~9999999范围内的数字,再给出若干个目标数字target,要求判断给出的这5个数字中是否包含target。

输入:47894 88 291 99 65535

target:6

聪明的小伙伴大眼一看,“哎!这不是跟上题一样吗,看我继续使用一个数组解决它!”。

稍等稍等,你再仔细看看,范围是0~9999999,你真的要开一个长度为10000000的数组嘛,虽说哈希表就是以空间换时间的思想,但是也不能这么浪费啊,仅仅是为了判断5个数字中是否包含target,就开一个这么长的数组,实在是不值得!那么,就没有更好的办法了吗?答案是有的!

我们依然是开一个长度为10的数组,但是,我们将输入的每个数都对10取余,将得到的值作为数组的索引值,如图

在这里插入图片描述

我们依然只需要判断arr[6]是否为target就能直接判断这堆数字里是否包含target,怎么样,是不是很神奇!!

上面的取余10就是我们自定义的哈希函数,我们将5个较大的数通过函数散列到0~9的范围内。直接取余算是最简单的哈希函数了,在实际的算法或者应用中,通常不会采用如此简单的函数进行元素的散列,因为不能很好地将元素散列开而导致严重的哈希冲突

哈希冲突

此时又有一位聪明的同学瞪着目光炯炯的大眼说:”哎!你这有问题啊哎,要是有两个数同时映射到了同一个位置怎么办?比如再加两个数 6 和 66, 该怎么判断这堆数字是否包含6啊?“

非常好,一看这位同学就是认真听讲了在这里插入图片描述

是的,当两个或多个数字用哈希函数后的散列值相同,就导致了冲突,也就是所谓的哈希冲突

那么怎么解决呢?这里给出一种叫链地址法的解决方法,请看图

在这里插入图片描述

由图可以看出,此时数组里保存的不再是数字,而是链表头节点的引用。链地址的原理就是,当出现哈希冲突的时候,将出现冲突的元素作为链表的节点链接在对应索引指向的链表的尾部。

依然是上文的那道题,输入增加了6和66两个数字。输入并计算出每个元素的哈希值后就链接在对应索引所指向的链表中。最后通过查看数组arr[6]是否为空,不为空的话遍历链表的每一个元素,与目标target相比,如果相同就说明这堆数字中包含目标target。

解决哈希冲突的方法有很多,比如开放定址法、链地址法、再哈希法等,有兴趣的同学可以自行查阅资料。值得一提的是,HashMap采用的是链地址法


添加元素

芜湖~终于到了大家期待的环节,接下来咱们就来唠唠HashMap添加元素以及扩容的流程,坐好!发车了

Alt

首先,来看看HashMap的继承图

在这里插入图片描述

了解类的继承层次能使我们看源码时思路更清晰。快捷键:选中类 ctrl + alt + u

接着,来看看HashMap中有哪些成员变量

// table数组的默认容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    
// table数组的最大容量,如果任何一个带参数的构造函数隐式指定了更高的值,则使用该容量
static final int MAXIMUM_CAPACITY = 1 << 30;

// 默认加载因子(可以使用有参构造器自定义)
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 当table某个索引位置对应的链表长度大于等于此变量数值时,转换为红黑树
static final int TREEIFY_THRESHOLD = 8;

// 当table某个索引位置的链表已经转换为红黑树,但是冲突元素的个数少于等于此变量数值时,转换为链表
static final int UNTREEIFY_THRESHOLD = 6;

// 判断将链表转换为红黑树时,table数组的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;

// 用来在散列的位置存储元素的数组
transient Node<K,V>[] table;

// 保留缓存,以及为了重写AbstractMap的keySet() 和 values()方法
transient Set<Map.Entry<K,V>> entrySet;

// 当前HashMap容器中元素的数量
transient int size;

// 该HashMap在结构上被修改的次数(添加元素,删除元素,扩容等都是结构修改)
transient int modCount;

// 要调整容量大小的下一个值(容量负荷系数),其实就是table数组长度的阈值,size大于此长度后扩容
int threshold;

// 加载因子变量
final float loadFactor;

这里先简单介绍一下这些成员变量的作用,后续会有详细说明。

笔者在后续叙述中会使用到容器table以及HashMap三个词汇,其中HashMap所含元素代表HashMap中添加的所有元素,包括数组中以及链表红黑树中的元素。而容器包含的元素就与HashMap包含的元素等价

其中,Node<K, V>为HashMap的内部类,其源码为

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        // Objects.hashCode()是本地方法,该方法保证对同一个对象的引用调用此方法得到的哈希值相同
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

由Node的equals方法可以看出,两个节点相同等价于两个节点的key和两个节点的value都相同,在后续的添加元素以及判断是否出现哈希冲突时会用到此方法,同key不同value的连续添加与不同key散列到同一个位置导致的冲突有不同处理逻辑

其实除了Node类,还有个TreeNode类,主要用于红黑树的节点,但因代码量较多故没有贴出来。不过在此占个坑,未来博主会专门出一期关于红黑树原理的文章!

再来看看HashMap的构造器

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
}

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

由源码可以看出,用户可以自定义HashMap的容量以及加载因子,如果使用无参构造器,则使用默认容量默认加载因子

同时,在构造器中也会调用tableSizeFor() 方法来计算下一次扩容后列表的长度

在tableSizeFor(int cap)方法中,首先会将参数cap减1,然后通过位运算将cap的高位的1都变成1,低位都变成0。这样得到的结果是一个大于等于cap的最小的2的幂次方数。

认真看的同学也许发现了,在这些构造器中并未对成员变量Node<K,V>[] table进行初始化,其实HashMap采用的是延迟初始化,table的初始化是在HashMap真正添加元素时进行的

put()

看完了构造器,咱们从添加元素的调用链来具体分析元素的添加过程

public V put(K key, V value) {
	return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

put()方法中又调用了putVal()函数,该函数的的各个参数的含义为

  • hash:由key通过hash()函数计算出的在table列表中的索引值,hash()函数使得计算出的索引尽可能分散开
  • key:Node节点的key
  • value:Node节点的value
  • onlyIfAbsent:是否仅在键值对在HashMap中不存在时才插入。如果为true,则只有在HashMap中没有与给定键关联的值时才插入。如果为false,则不管HashMap中是否已经存在与给定键关联的值,都会插入新的键值对。
  • evict:是否允许在插入新键值对时可能引发的删除操作。如果为true,则允许删除操作。如果为false,则不会执行删除操作。

这个方法的主要功能是根据给定的hash值,将键值对插入HashMap中的内部数组中的对应位置。如果相同位置已经存在键值对,根据onlyIfAbsent参数的值决定是否替换旧值。如果evict参数为true,并且HashMap的容量已经达到了阈值,会执行可能的删除操作。

接着进入到putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) 方法中

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;									 // 步骤1
    if ((tab = table) == null || (n = tab.length) == 0)                      // 步骤2
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)								 // 步骤3
        tab[i] = newNode(hash, key, value, null);
    else {																	 // 步骤4
        Node<K,V> e; K k;
        if (p.hash == hash &&												 // 步骤4.1
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)										 // 步骤4.2
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {							 // 步骤4.3
                if ((e = p.next) == null) {									 // 步骤4.3.1
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&										// 步骤4.3.2
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key 					    // 步骤5
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)						   // 步骤5.1
                e.value = value;
            afterNodeAccess(e);											   // 步骤5.2
            return oldValue;
        }
    }
    ++modCount;															   // 步骤6
    if (++size > threshold)												   // 步骤7
        resize();
    afterNodeInsertion(evict);											   // 步骤8
    return null;
}

下面来理一理添加元素的整个过程

  1. 创建对数组table的新的引用tab,p为hash对应索引的元素,如果该位置为空则为null

  2. 首先判断table是否已经初始化过,因为HashMap的table是延迟初始化的,如果没有初始化,调用resize()方法初始化(扩容也发生在该方法中)

在这里插入图片描述

  1. 将table的长度hash进行与操作,目的是去掉hash的高位1,使得计算后的索引位置是处于0~到n之间的数字,如果索引位置没有节点即空的,将需要添加的节点插入在此位置,否则进入步骤4

在这里插入图片描述

  1. 进入此步骤说明上一步计算出的索引位置上已经有节点了,说明发生了哈希冲突

4.1 判断要添加的节点的key与在该索引位置上的节点的key是否是同一个对象,保留该索引位置上的引用.如果是则进入步骤5

在这里插入图片描述

4.2 如果该索引位置上的节点是TreeNode, 说明此位置已经进行过链表到红黑树的转换了,将该节点添加到红黑树中(这种画法只是为了表示这是红黑树,真实形态并不是如此),否则进入步骤4.3

在这里插入图片描述

4.3 进入此步骤说明该索引位置还未由链表转化为红黑树。遍历链表

4.3.1 判断冲突位置上一个节点的下一个元素是否为空(为空则表明上一个节点是链表尾),如果为空则新创建一个node节 点作为链表尾,并判断当前索引位置上的链表的长度是否大于等于8,如果是,则进入treeifyBin(tab, hash) 函数来判断是否要把链表转换为红黑树

在这里插入图片描述

4.3.2 如果不是,则像步骤4.1一样判断该位置上的节点与要添加的节点的key是不是同一个对象,如果是,结束遍历,进入步骤5的if语句中。否则进入链表的下一个节点重复4.3.1和4.3.2的逻辑直到链表尾。

  1. 如果是由hash计算出来的索引位置上的节点或者是索引位置上的链表中的节点的key与要添加的节点的key同一个对象

    5.1 由参数onlyIfAbsent来决定是否用要添加的节点的新value来覆盖掉原来的value(onlyIfAbsent在前面介绍过)

    5.2 没有处理逻辑

  2. 对modCount进行自增操作,表示HashMap的结构修改过一次

  3. 判断添加一个节点后是否需要扩容

  4. 没有处理逻辑

步骤4.3.1中,通过要添加元素的hash值确定元素在数组中的索引,如果该位置已经发生冲突,则会遍历索引位置上的链表。如果最终

走到了链表尾部则会将当前要添加的元素插入到链表尾部。在插入后会判断当前链表的长度是否大于等于8来尝试将链表转换为红黑树 。为

什么说是尝试呢?咱们来进入到treeifyBin(tab, hash)函数中一探究竟

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 即使当前索引上的链表的长度已经大于等于8,如果表的容量小于64,不会进行链表到红黑树的转换
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        // 走到这里说明表的容量已经大于64了
        // 将链表上的节点都转换为TreeNode节点
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            // 将由TreeNode构成的链表转换为红黑树
            hd.treeify(tab);
    }
}

那么到这里,添加元素的操作就讲述完了~~~不过,最核心的扩容操作还没讲呢!坐稳了,咱继续~

在这里插入图片描述

resize()

此方法也就是扩容方法了,那么什么时候会触发扩容机制呢,那肯定是添加元素的时候嘛!因为只有添加元素时才会出现容量不足的情

况。其实在刚刚的putVal()方法中,已经调用了几次resize()方法了,比如步骤2中给table数组初始化调用。步骤7中添加了元素后,

判断是否超过了容量。那么接下来,咱们就来看看resize()方法里究竟做了哪些事~

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;// threshod是扩容阈值,即HashMap中的元素个数大于此值会触发扩容
    int newCap, newThr = 0;
    // 判断是否已经进行过了初始化,即为table数组分配物理内存
    if (oldCap > 0) {
        // 说明table数组已经初始化,如果table的长度已经大于等于MAXIMUM_CAPACITY = 2^30, 则将阈值变为2^31 - 1
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 将table的新长度设置为原长度的2倍,判断新长度是否小于2^30 且 原长度大于等于默认长度16
        // 如果是则将原扩容阈值 * 2 作为新的扩容阈值
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; 
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        // 进入这里说明用户调用了有参构造器,指明了HashMap的容量。oldThr也在构造器中被赋值
        // 将扩容阈值作为table数组的长度
        newCap = oldThr;
    else {               
        // 进入这里说明用户调用的是无参构造器,扩容阈值threshold未被初始化
        newCap = DEFAULT_INITIAL_CAPACITY;  //DEFAULT_INITIAL_CAPACITY = 16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 0.75f * 16
    }
    if (newThr == 0) {
        // newThr并未被设置。所以在这里设置
        float ft = (float)newCap * loadFactor;// 新长度 * 加载因子
        // 如果table的新长度小于2^30且ft也小于2^30, 则新的扩容阈值就为ft
        // 否则就为2^31 - 1,即int所能表示的最大正整数
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    // 创建新的Node数组,以上面计算的新的长度作为数组的长度。
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 将原table数组中的元素转移到新数组中
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

在上述源码中已经以注释的方式将扩容的细节讲述清楚了,下面我就再简单总结一下

  1. 首先判断table数组是否初始化过。如果是则进入步骤2。接着,如果用户创建HashMap指明了容量加载因子也可以指明,如果指明,那么后面的加载因子都替换为用户指明的加载因子),那么在第一次进入resize() 函数里会将扩容阈值threshold作为table数组的新长度,(扩容阈值在有参构造器中被计算,其值是大于等于用户指明的容量的最小的2的幂次方倍),而新的扩容阈值则由table数组新长度 * 加载因子计算得出。如果用户调用的是无参构造器,则table数组的新长度为默认长度16,扩容阈值threshold则依然由table数组新长度 * 加载因子计算得出

  2. 如果table已经初始化过,那么判断table数组的原长度是否大于等于MAXIMUM_CAPACITY(230),如果是,则将扩容阈值设置为Integer.MAX_VALUE(231 - 1)。如果不是则接着将table数组的新长度设置为原来长度的2倍,并判断新长度是否小于230 且 新长度大于等于 16,如果是,则将原扩容阈值 * 2作为新的扩容阈值。如果不是,那么扩容阈值还是table数组新长度 * 加载因子。顺便提一下,table数组的最大长度就是MAXIMUM_CAPACITY(230),到达这个长度就不会扩容了。

  3. 创建一个长度为新长度的新数组,并将原数组中的元素迁移到新数组中,采用遍历数组的方式迁移,迁移流程如下

3.1 如果该索引位置上不为空,但是只有一个节点。那么将该节点的hash值与新长度进行与操作计算出该节点在新数组的索引,并将该节点插入到新索引上。

在这里插入图片描述

3.2 如果该索引位置上的节点是TreeNode,说明该节点上冲突较多,已经由链表转换为了红黑树。那么将红黑树上的所有节点分成两拨,高地址的放一起,低地址的放一起(高地址和低地址什么意思后面讲)。如果高地址或低地址的这些节点个数不大于UNTREEIFY_THRESHOLD=6(将红黑树转换为链表的阈值)就把这些节点链接为链表,如果大于6,就依然为红黑树形式

​ 再次声明,红黑树的形式并不是如画的一致,笔者只是用此来代表红黑树

在这里插入图片描述

3.3 如果该索引位置上是链表,那么与3.2处理过程一样,依然是把这个节点上的链表分成两拨,高地址的放一起,低地址的放一 起。最后迁移到新数组中

在这里插入图片描述

到此,HashMap扩容的流程就完了~

HashMap的巧妙设计

在步骤3中,如果某个索引位置上不止有一个节点,就会把这些元素分为分为两拨,高地址的一拨,低地址的一拨。现在咱就来解释一下,高低地址是什么意思。

首先,每个节点在被加入HashMap前,都会计算根据key计算一个hash值

static final int hash(Object key) {
	int h;
	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

假设这个值的二进制形式为

1 0 1 1 0 1 0 1 1 0 1 1 0 0 1 1 0 1 1 0 1 1 1 0 1 0 1 0 1 1 0 1

我们知道,节点被添加进table中前会先计算该节点位于table数组中的索引位置对吧!

这个索引位置 = (table数组长度 - 1) & hash

假设长度现在是8,那么与之后的值就为5

1 0 1 1 0 1 0 1 1 0 1 1 0 0 1 1 0 1 1 0 1 1 1 0 1 0 1 0 1 1 0 1
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 与操作
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 = 5

这样计算的原因就是将要添加的元素所在的索引位置控制在table数组的长度范围之内,这样一来,最终元素的索引位置其实取决于hash的低3位的数字!那么前面提到,table数组每次扩容后的长度都是2的幂次方倍,其二进制形式就是最低位的0变成了1,比如8扩容至16

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1

再与hash进行与操作后

1 0 1 1 0 1 0 1 1 0 1 1 0 0 1 1 0 1 1 0 1 1 1 0 1 0 1 0 1 1 0 1
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 与操作
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 1 = 13

那么,最终元素的索引位置取决于hash低4位的数字!而且你也可以发现,当扩容后,某索引位置上的元素在新数组的位置只有两种可能。一种是原索引位置,一种是在原索引 + 原数组长度位置这取决于扩容后新长度二进制形式最高位的1对应的hash二进制形式位置上是0还是1

// 如果是0
1 0 1 1 0 1 0 1 1 0 1 1 0 0 1 1 0 1 1 0 1 1 1 0 1 0 1 0 0 1 0 1
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 与操作
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 1 = 5

// 如果是1
1 0 1 1 0 1 0 1 1 0 1 1 0 0 1 1 0 1 1 0 1 1 1 0 1 0 1 0 1 1 0 1
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 与操作
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 1 = 13 = 5 + 8

回到步骤3,在原索引位置上的就为低地址,在原索引 + 原数组长度位置的就是高地址。这样做的目的是将原来的数组中的节点按照哈希值的高位部分进行重新分配,放置在新的更大容量数组的正确位置上,以保持节点在数组中的分布均匀

看到这,你也许就理解了为什么HashMap的底层数组table的长度要设置成2的幂次方倍了。首先,在计算要添加元素的索引位置时可以使用位运算来提高性能,然后在扩容后节点迁移时,只需要进行简单的位运算,降低了元素迁移成本。如果容量不是2的幂次方,而是其他任意值,那么计算节点在新数组中的索引位置会更加复杂。需要进行昂贵的取模运算或者其他复杂的计算操作,从而导致扩容时的成本增加。

非常感谢你能看到这里!!那么HashMap添加元素与扩容的流程与细节就讲完了,如果有错误的地方欢迎与博主沟通讨论!!!如果你觉得这篇文章对你有帮助,不妨点个小小的关注或者收个藏!!

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值