HashMap-----put()

1. 注释翻译

/**
     * 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>.)
     * 前一个值与key相关联 ,或null如果没有key的映射。(A null返回也可以指示以前关联的地图null与key。)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
        		// hash(key):根据key计算了当前Node的hash值,用于定位对象在那个结点上
    }

  通过注释我们可以了解到该方法的作用是:将指定的值与此映射中的指定键相关联。 如果地图先前包含了该键的映射,则替换旧值。深入put方法中发现只调用了putVal方法,我们进入到putVal方法中再去观察一下。

2. 源码剖析

   /**
     * 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
     * 						如果该值为true,则不需要改变现有的value值
     * @param evict if false, the table is in creation mode.
     * 				如果该值为false,则表处于创建模式
     * @return previous value, or null if none
     * 			如果key键有映射value,则替换就value,并返回旧value;如果没有则返回null
     */
     // 此处的hash值是通过key值计算出的位置
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 这里的tab为null时,证明哈希表还未初始化
        if ((tab = table) == null || (n = tab.length) == 0)
        	// 调用resize方法进行哈希表的初始化操作
            n = (tab = resize()).length;	// 将初始化的哈希表容量赋给n
        // 当前需存放KV值的桶为空
        if ((p = tab[i = (n - 1) & hash]) == null)
        	// 创建Node结点(KV值已存放),作为桶的头节点
            tab[i] = newNode(hash, key, value, null);
        else {
        // 哈希表已经初始化,且当前需存放KV值的桶不为空
            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) {
                    	// 找到最后一个结点,将当前Node结点挂在链表后
                        p.next = newNode(hash, key, value, null);
                        // 如果当前链表个数到达树化阈值,调用树化方法treeifyBin将链表树化
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                       	// 结点已经挂载且链表已树化(达到树化阈值),跳出循环
                        break;
                    }
                    // 当前链表中存在相同的key,跳出循环
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    // 实现往后指向
                    p = e;
                }
            }
            // 链表存在相同key,直接覆盖value,返回旧value
            if (e != null) { // existing mapping for key
            	// 保留旧value值
                V oldValue = e.value;
                // 如果onlyIfAbsent值为true或oldValue值为null,则不需要替换旧值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                
                afterNodeAccess(e);
                // 返回旧值
                return oldValue;
            }
        }
        // HashMap被结构性修改的次数加1(结构性修改是指改变了KV映射数量的操作或修改了HashMap的内部结构)
        ++modCount;
        // 当添加结点后整个HashMap的元素若超过容量时,进行扩容
        if (++size > threshold)
            resize();	// 扩容
        afterNodeInsertion(evict);
        return null;
    }

注意:

  1. 我们深入到putVal中会发现resize()方法有两个任务:创建初始存储数组(数组为null时)扩容(容量不满足需求时)。这个方法我在其他博文有总结,如果有兴趣的,可以单击查看。

  2. 具体键值对在哈希表中的位置并不是由key值通过hash()方法计算的数组,而是通过(n - 1) & hash,这样我们会发现会将高位的数据移到低位,这是因为有些数据计算出的哈希值差异主要在高位,而HashMap 里的哈希寻址是忽略容量以上的高位的,那么这种处理就可以有效避免类似情况下的哈希碰撞。

3. 方法描述

  put方法将KV放在map中。如果,该key已经存放在map中,则用新的value值直接替换旧value值。
  如果该key已经存放在map中,则返回其映射的旧值;如果不存在,则返回null,表示没有该key的映射,也有一种特殊的情况,原来的映射是key-null,也会返回null。
  put方法存储元素有两种情况:① 键值对在当前桶中是以链表形式存放的。② 键值对在当前桶中是以红黑树形式存放的。所以在插入时就需要判断当前桶的存储形式,需要特别关注链表存储时,超过TREEIFY_THRESHOLD,要进行树化操作。

4. put执行流程

  1. 若HashMap还未初始化,调用resize进行初始化
  2. 通过hash值得到所在桶的下标
    ① 若桶为空,将节点直接作为桶的头节点保存
    ② 若桶不为空
     a. 如果桶中链表已经树化,使用树的方式添加结点
     b. 未树化,将新节点以链表的形式尾插到最后
       i. 添加元素后,链表个数binCount >= 树化阈值-1,尝试进行树化操作
    ③ 添加元素后计算整个哈希表的大小,若超过threshold(容量*负载因子),进行resize扩容操作。

5. 流程图

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/meng_lemon/article/details/88906980