浅谈 HashMap(二):put 插入方法源码分析

接上文:浅谈 HashMap(一):数据结构底层实现方式

本文基于 JDK1.8


这节讲讲 HashMapput 方法,看似简单的一个存值方法,实则细节满满,且听我娓娓道来
话不多说,先看看源码👇

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

方法内部又调用了一个 putVal 方法,一共 5 个参数,我们先猜猜这几个参数是干嘛的。第一个参数是根据 key 调用了hash()方法,此参数应该是用来确定 keytable(这个属性参照上一节)数组中的位置的;第二个参数就是传入的 key 了;第三个参数即传入的 value 了,下面我们通过源码来看看👇

	/**
	 * Implements Map.put and related methods
	 *
	 * @param hash key的hash值
	 * @param key 键
	 * @param value 准备插入的值
	 * @param onlyIfAbsent 如果为true,则不修改已存在的值
	 * @param evict 如果为false,则table数组处于创建模式
	 * @return 旧的值,如果没有则返回null
	 */
	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
	// 定义局部变量,接收table数组
	// 这种全局变量赋值给局部变量操作的写法,是一种编码习惯,便于代码可读性及调试
	Node<K,V>[] tab; 
	// 局部变量,接收table数组位置上的Node节点(如果已存在节点)
	Node<K,V> p; 
	// 局部变量 n,table数组长度
	// 局部变量 i,根据hash值(由key计算而来,后续讲)确定在table数组中的位置
	int n, i;
	// 上节讲到table数组在第一次使用时初始化,源于此处
	// 如果table数组为空,则通过resize方法初始化
        if ((tab = table) == null || (n = tab.length) == 0)
	    // resize方法后续有专题讲
            n = (tab = resize()).length;
	// 判断当前table数组位置是否已有值
	// 如何根据hash值确定在table数组中位置的呢?后续有专题讲
        if ((p = tab[i = (n - 1) & hash]) == null)
	    // 如果table数组此位置没有值,则新建Node节点存于此处即可
            tab[i] = newNode(hash, key, value, null);
	// 如果table数组此位置已经有值,则执行下面代码
        else {
	    // 局部变量,接收已存在的Node节点
            Node<K,V> e; 
	    // 局部变量,接收键(key)
	    K k;
	    // 判断key值是否相等,相等则将原Node赋值给局部变量 e
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
	    // 判断已有节点的存储形式是否为 tree(红黑树),如果是tree类型,则给红黑树中新增一个Node(红黑树的增删此处不讲)
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
	    // 除去上面两种情况,则table数组此位置的节点为链表存储形式
            else {
		// 从此处循环可知,HashMap在JDK1.8的链表插入为 [尾部插入]
                for (int binCount = 0; ; ++binCount) {
		    // 循环遍历链表,直到最后一个Node,新建一个Node存值
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
			// 当链表长度大于 树转换阈值(8)时,链表转换为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
		    // 遍历链表时,判断链表中已存在Node的key是否与插入的key相等
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
	    // 判断插入的key是否为已存在的key
            if (e != null) { // existing mapping for key
		// 从此处可知,前面为何要定义两个局部变量 p 和 e,为了返回旧的value值
                V oldValue = e.value;
		// 根据传入的参数,决定是否修改已存在的值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
		// 如果插入的key已存在,返回已存在的旧的value值
                return oldValue;
            }
        }
	// HashMap的全局变量,每次操作时加 1
        ++modCount;
	// 先对HashMap的容量 +1,判断是否达到扩容阈值(最大容量 * 负载因子0.75)
        if (++size > threshold)
	    // 扩容,重置HashMap大小,后续有专题讲解
            resize();
        afterNodeInsertion(evict);
	// 如果插入的key是新的,则返回null;对比上面,如果key已存在,则返回旧的value
        return null;
    }

put 方法的源码如上,笔者已经添加了详细的注释。其中还有一些没有展开讲的后续出专题
主要有:如何扩容 resizehash(key) 确定数组中位置的实现方式?

posted @ 2021-08-09 14:31  超级鲨鱼辣椒  阅读(140)  评论(0编辑  收藏  举报