【JAVA】HashMap源码探索

19 篇文章 0 订阅
9 篇文章 0 订阅

 

hashmap的源码在java的面试中一直是一个很关键的部分,在搜索面试题的时候我们经常会看到类似这样的题目:

  • hashmap的底层数据结构是什么?
  • 描述一下hashmap put方法的过程?

很多时候我们好像都只顾着使用它,而不清楚底层实现,还有的时候我们知道一鳞半爪,但是表示不出来具体的内容。

今天我准备从源码理一遍,HashMap的实现细节。

 

一. 什么是Map?

 根据Map源码上的注释可以得到:

    1.Map是一个接口,他是key-value的键值对,一个map不能包含重复的key,并且每一个key只能映射一个value;

    2.Map接口提供了三个集合视图:key的集合,value的集合,key-value的集合;

    3.Map内元素的顺序取决于Iterator的具体实现逻辑,获取集合内的元素实际上是获取一个迭代器,实现对其中元素的遍历;

    4.Map接口的具体实现中存在三种Map结构,其中HashMap和TreeMap都允许存在null值,而HashTable的key不允许为空,但是HashMap不能保证遍历元素的顺序,TreeMap能够保证遍历元素的顺序。

/**
 * An object that maps keys to values.  A map cannot contain duplicate keys;
 * each key can map to at most one value.
 *
 * <p>This interface takes the place of the <tt>Dictionary</tt> class, which
 * was a totally abstract class rather than an interface.
 *
 * <p>The <tt>Map</tt> interface provides three <i>collection views</i>, which
 * allow a map's contents to be viewed as a set of keys, collection of values,
 * or set of key-value mappings.  The <i>order</i> of a map is defined as
 * the order in which the iterators on the map's collection views return their
 * elements.  Some map implementations, like the <tt>TreeMap</tt> class, make
 * specific guarantees as to their order; others, like the <tt>HashMap</tt>
 * class, do not.
 */

二. HashMap的概念

1. 什么是哈希表

哈希表(HashTable,散列表)是根据key-value进行访问的数据结构,他是通过把key映射到表中的一个位置来访问记录,加快查找的速度,其中映射的函数叫做散列函数,存放记录的数组叫做散列表,哈希表的主干是数组。

 上面的图中就是一个值插入哈希表中的过程,那么存在的问题就是不同的值在经过hash函数之后可能会映射到相同的位置上,当插入一个元素时,发现该位置已经被占用,这时候就会产生冲突,也就是所谓的哈希冲突,因此哈希函数的设计就至关重要,一个好的哈希函数希望尽可能的保证计算方法简单,但是元素能够均匀的分布在数组中,但是数组是一块连续的且是固定长度的内存空间,不管一个哈希函数设计的多好,都无法避免得到的地址不会发生冲突,因此就需要对哈希冲突进行解决。

    (1)开放定址法:当插入一个元素时,发生冲突,继续检查散列表的其他项,直到找到一个位置来放置这个元素,至于检查的顺序可以自定义;

    (2)再散列法:使用多个hash函数,如果一个发生冲突,使用下一个hash函数,直到找到一个位置,这种方法增加了计算的时间;

    (3)链地址法:在数组的位置使用链表,将同一个hashCode的元素放在链表中,HashMap就是使用的这种方法,数组+链表的结构。


--------------------- 
作者:qq_41786692 
来源:CSDN 
原文:https://blog.csdn.net/qq_41786692/article/details/79685838 
版权声明:本文为博主原创文章,转载请附上博文链接!
 

2. 什么是HashMap?

HashMap是基于哈希表的Map接口的实现,提供所有可选的映射操作,允许使用null值和null键,存储的对象时一个键值对对象Entry<K,V>;

HashMap是由数组,链表和红黑树实现的(jdk1.8为了优化查找性能将一定数量的链表转化为红黑树,使得时间复杂度下降为O(logn))。如下图就是HashMap的数据结构。

当然红黑树是要有一定数量才能从链表转化的,这个图只是简单示意在HashMap的数据结构大概上是这样的。

而这个数据结构是如何通过代码实现的呢?我们可以从源码中来寻找答案:

a. HashMap的初始化

Map<String,String> map = new HashMap<String, String>();

多数情况我们是通过这样的方法来创建hashmap的,而还有些时候我们会使用ConcureentHashMap来新建Map,但是这两种方法是不完全一样的,因为通过ConcurrentHashMap是另一种HashMap,这我们之后细究它。


    /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
    static final int MIN_TREEIFY_CAPACITY = 64;


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

在map对象初始化的时候,只是初始化一些常数。

b. put方法。

初始化完对象以后,接着我们就要向map中存入元素,以一次插入的经过来探究HashMap上面画的哪些数据结构究竟是如何实现的。

map.put("put", "putvalue");
    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

在put 方法里,调用了HashMap的putVal方法。在这个方法传参的时候,使用了hash(key)这个方法根据key生成一个数,这个数是做什么用的之后会详细介绍,此时我们接着往里看代码。

因为在java中一切皆对象,所以当我们要存一个键值对的时候,当然也是作为一个对象把它存进去,而我们的数组的存放类型自然也是键值对的对象Node<K,V>。table是类的成员变量,他的作用域是整个对象,并且用transient关键字修饰(这个关键字的意思是被这个关键字修饰的对象无法序列化)。

/**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    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) 
        //用成员变量table给tab赋值,判断他是否为空或者里面没有任何数据。
            n = (tab = resize()).length; //数组为空则对数组进行初始化
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        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;
                    p = e;
                }
            }
            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;
    }

从我们之前的数据结构图里可以看出我们的Node对象自然是首先需要被存放在数组里的,但是如果我们没有一个数组或者数组的大小为0,那么我们首先需要对数组进行初始化。在java中数组和list的差别在于,数组首先是有一个给定的大小,那么我们先看看初始化的过程是怎么进行的。

需要事先说明的是,resize()这个方法不仅进行数组的初始化,还可以进行数组的扩容,现在我们先看初始化的部分。

 final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table; //把我们现有的数组赋值给oldTab
        int oldCap = (oldTab == null) ? 0 : oldTab.length; // 如果oldTab为空则oldCap(旧容量)为0,不然旧容量的值就是oldTab的实际大小。
        int oldThr = threshold; // 旧的边界值就是现有的threshold(边界值),超过这个值就需要扩容。
        int newCap, newThr = 0;// 初始化新数组的容量和边界值都为0
        if (oldCap > 0) {
            // 如果旧数组的容量大于0,则说明是扩容,此时我们不讨论这块代码。
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            // 如果旧边界值大于0 且旧容量为0,那么就用旧边界值来直接赋值新容量(这种场景我们此时也不讨论)
            newCap = oldThr;
        else {               
            // zero initial threshold signifies using defaults
            // 如果旧容量和旧边界值都为0,说明这是一个空map,需要被初始化
            newCap = DEFAULT_INITIAL_CAPACITY; 
            //从上面的代码中我们可以看出DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
            //即1向左位移4位即,1==>10000,这是16的二进制表示,即新容量为16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            //而新边界值则需要经过计算并且转化为整型DEFAULT_LOAD_FACTOR在代码里可以看到是0.75
            //即0.75*16=12,只要容量被占用超过12,我们就需要进行扩容了
        }
        if (newThr == 0) { // 如果此时新边界值还未0则做下面操作(此时我们不多做研究)
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;// 将计算完成的新边界值赋值给成员变量threshold
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//用新容量来初始化新数组
        table = newTab; // 将新的数组赋值给成员变量数组
        if (oldTab != null) { //不仔细也能看出是如果原来的map里就有数据,扩容后需要对数据进行搬迁,这个我们也暂时不讨论,这次我们就只讨论初始化
            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;//返回新数组
    }

首先如果目前map是空map,我们要对他做的是初始化,则先要给他确定一个初始的容量(即16)和一个初始的边界值(即12),用这个容量生成新数组,并把新数组和新边界值都传递给成员变量,这就完成了初始化。

让我们继续看回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; // 初始化现在已经结束
        if ((p = tab[i = (n - 1) & hash]) == null) //判断 n-1与hash做与运算得到的数值对应数组下标的位置是否为空,并把数组该下标的值取给p
            tab[i] = newNode(hash, key, value, null); // 如果该下标位置为空,则生成一个新的node插入
        else { // 如果不为空即发生了hash冲突,则分三种情况
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))) 
                // 1. put的key与节点中的key完全相同,则用p赋值node
                e = p;
            else if (p instanceof TreeNode)
                // 2. put的key和节点中的key不完全相同,且节点上保存了一个红黑树
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 
                // 对红黑树进行插入节点操作,如果返回值为null则说明红黑树里没有key值和插入的key值相等的节点,插入已经成功(具体插入步骤之后讨论)
            else {
                // 3. put的key和节点中的key不完全相同,且节点上保存一个链表或者还没有链表
                for (int binCount = 0; ; ++binCount) {// 循环链表
                    if ((e = p.next) == null) { //先从p的子节点出发,并将e定位到p的子节点,如果p没有子节点
                        p.next = newNode(hash, key, value, null);//用新的键值对创建一个节点并把这个节点赋值为p.next,即作为p的子节点
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            // 如果此时的链表长度已经超过一个给定的值,那么将链表转化为红黑树,并跳出循环。
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 若e的hash等于传入hash,key与传入Key完全相等,则跳出循环
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    //否则,将e赋值给当前节点p并进入下一次循环,此时binCount会增大一位
                    p = e;
                }
            }
            //从上面的代码可以看出,如果e为null则,key在map中并不存在,直接生成新节点进行插入了,但是如果key存在,e就会是那个存在的节点。
            // 如果onlyIfAbsent为false或者节点原来的value为空,就用新值进行覆盖,覆盖后返回旧值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);//实际上并没有进行任何操作
                return oldValue;
            }
        }
        
        // 上面说如果是覆盖会直接返回,如果是新插入节点,则会增加节点数,并当当前数组利用的长度超过边界值(即之前计算的12,如果扩容会发生改变,但是基本上都是容量*0.75),则进行扩容。
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);//此处没有进行任何操作
        return null;//如果是插入了新节点,那么会返回空
    }

 if ((p = tab[i = (n - 1) & hash]) == null) :这步实际需要详细解释,正好在这个位置也说一下hash这个值是怎么来的。在我们进入put方法的时候,实际上我们的第一个参数是由hash(key)计算得到,正好就是此处的hash变量。那么这个参数是做什么用的呢?

从上面的初始化过程可知,我们初始化了一个容量为16的数组,数组是不能改变长度的,那么我们的必须为每一个插入的对象指定一个小于16的下标,这个下标是怎么计算的呢?是用n-1与hash这个值做且运算得到的,我们知道&是位运算,于是我们先把n-1转换为二进制,即是01111。如果一个数和01111进行且预算是怎么保证这个得到的数一定是小于等于15的呢?

因为我们做且运算,所以01111左边的位全部填充为0。又因为0&1或0都是0,所以其实最多只有右边的四位数会生效,总之就是截取了这个二进制数的最后四位。之前我们看到了,16的二进制为10000,则四位二进制数,一定小于16,即小于等于15,1111是这个方法能取到的最大二进制数。

看起来这个计算方法和hash%16异曲同工。

而我们需要看一下hash这个值是如何取得的:

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

我们知道,java中的上帝对象Object有一个方法是hashCode(),这个方法会返回一个对象对应的32位数。这里的方法是先获取key的32位hashCode,然后赋值给h,再对h右位移16位,即去掉最左的16位数,用右移后的结果对h本身进行异或预算,即用高16位和低16位进行异或运算。

为什么这样做呢?因为既然我们已经确定了数组的长度为16,那么我们就要尽可能的利用这16个空间,至少让我们的数据能够尽量平均的分布,也就是最后下标最好不要集中在某几个,其他几个下标完全没有反应。这就是我们为什么要使用异或^的原因。因为&和|都会让数值倾向1或0,只有异或^才能让1和0平均分布。如下图:

这样我们最后会获得一个32位数,来和n-1进行且运算,此时会得到一个小于等于15的下标。

我们首先判断我们给这个key计算的下标对应的空间是否已经存放了节点:

1. 如果没有,那么我们直接新建一个节点存放到该下标对应的空间中。

2. 如果有,那么分三种情况:

a. put的key与节点中的key完全相同,则用p赋值node

b. put的key与节点中的key不完全相同,且p中存放着红黑树,此时对红黑树做节点put,如果已有对应节点则取出对应节点,如果没有则将新节点插入红黑树并返回空。

c. put的key与节点中的key不完全相同,且p中存放着链表或者还没有链表,遍历列表,如果已有对应节点则取出节点,如果没有则将新节点作为最后一个子节点插入链表且返回空(此处需要判断是否需要变链表为红黑树)。

如果我们找到了key相同的对应节点,则对对应节点的值做覆盖,并把老值作为结果返回。

如果我们插入了新节点,我们首先判断是否需要扩容,需要则扩容。

 

最后我们再来说一下扩容是如何操作的,回到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.
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) { // 如果原有容量大于0,说明我们是做扩容
            if (oldCap >= MAXIMUM_CAPACITY) { // 如果此时容量已经达到最大,那实在没办法了,只能保持原有容量
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY) // 如果没有达到最大,那么新容量就是就容量的左移一位,即旧容量*2(二进制很好理解,10000(16)变成了100000(32)),并且新容量也小于最大容量,就把就边界值左移一位变成新的边界值。如果新容量已经等于最大容量暂且为0,之后重新计算。
                newThr = oldThr << 1; // double threshold 
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
        // 如果旧边界值大于0 且旧容量为0,那么就用旧边界值来直接赋值新容量(这种场景我们此时也不讨论)
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            // 初始化,我们已经讨论过了
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            // 如果新边界为0则用新容量重新计算(如果新容量等于最大值也属于这种情况)
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
                        //如果新容量等于最大值,就用Integer.MAX_VALUE做边界。
        } 
        threshold = newThr; // 把新边界赋值给成员变量threshold
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];// 用新容量生成一个新数组
        table = newTab; // 用新数组覆盖旧数组
        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) { //用节点的hash与原容量进行且运算得到的值如果为0
                                if (loTail == null) // 如果loTail就为空,则把e赋值给loHead
                                    loHead = e;
                                else
                                    loTail.next = e; //否则就把loTail的子节点指向e
                                loTail = e; // 最后把e赋值给loTail,即数组原来的下标
                            }
                            else {//用节点的hash与原容量进行且运算得到的值如果不为0
                                if (hiTail == null) // 如果hiTail为空则将e赋值给hiHead
                                    hiHead = e;
                                else
                                    hiTail.next = e; //否则就把hiTail的子节点指向e
                                hiTail = e;// 最后把e赋值给hiTail高位的尾巴,即数组的原来位置+旧容量
                            }
                        } while ((e = next) != null); // 直到遍历完成
                        if (loTail != null) {//如果链表放在lo系列里,那么原下标不变进行搬迁
                            loTail.next = null
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {//如果链表放在hi系列里,那么下标变为元下标+旧容量
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;//返回新数组
    }

大概理一下整个过程:

1. 如果是扩容,则判断现有的容量是否已经最大,最大则直接返回旧容量。

2. 如果现有容量没有到达最大,就把容量继续左移一位作为新容量,当新容量没有到达最大值是,边界值也是左移一位。

3. 如果新容量到达了边界值,那么新边界就设为最大整数。

4. 对现有数据进行搬迁。

搬迁过程如下:

1. 遍历链表,依次用节点的hash值与旧容量做且运算。

2. 如果且运算结果为0那么放入低位链表。

3. 如果且运算结果不为0那么放入高位链表。

4. 低位链表存入原下标,高位链表存入原下标+旧容量的下标中。

那么让我们来研究一下为什么是这么计算的:

首先假设我们的旧容量为10000(16),那么新容量则为旧容量左移一位即100000(32)。

此时旧的n-1为01111(15),那么新的n-1为011111(31)。

让我们看看旧n-1&hash和新n-1&hash有什么差别呢?

对就只是多了第五位这一位二进制。

那么我们用旧容量10000&hash就可以把这一位数单独提出来了,如果hash右往左数第五位为0,那么此时计算结果也为0.哪怕用新n-1&hash也是得到就下标,而如果计算结果不为0,那么理所当然的,新下标即为旧下标左边多出一位1,即下标增加了一个原有容量的数。

这样我们就能理解这个为什么这么写了。

三. 总结

分析到这里,我们应该已经能理解hashmap是如何由数组、链表和红黑树构成的,并且put操作是经过了哪些步骤吧。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值