源码梳理(5)HashMap的put方法

1 概述

源码使用的是JDK17
HashMap的数据存放简述

1 HashMap中存在一个属性名叫table的哈希表,结构类型是数组
2 哈希表这个数组,它的每一个下标位置存放的是Node类型的链表的首节点(链表在一定条件下会转为红黑树)
3 每一个Node实例都会有key和value的属性值,对应的是HashMap中的一条数据(也可以说一个Node就是一条数据)
4 为什么说Node是链表呢,因为Node中有一个属性next,它的类型也是Node,表示当前节点指向的下一个节点(也就是每个Node后面可能还链接着另一个Node)

HashMap中put方法的流程概览

1 通过hash方法根据传入key的hashcode重新计算hash值(6.2)
----目的:减少hash冲突的概率
2 当前的哈希表未进行初始化(首次添加元素),则进行初始化扩容(7.1)
----如果构建HashMap的时候没有设置容量,则会扩量到默认值16
----如果构建HashMap的时候设置了容量,则会扩容到构造函数计算出的扩容阈值threshold
3 计算key的哈希表下标,判断该位置是否已经有值(7.2)
3.1 当前下标位置没有Node,直接添加元素(7.3)
3.2.1.1 当前下标位置有Node,该Node的key和当前key一致(7.4)
3.2.1.2 当前下标位置有Node,Node是红黑树类型,遍历红黑树(7.5)
----找到红黑树节点中的key和当前key相等的Node
----红黑树中不存在节点中的key和当前key相等的Node,则新建一个节点,插入到红黑树中
3.2.1.3 当前下标位置有Node,Node是链表,遍历链表(7.6)
----找到链表节点中的key和当前key相等的Node
----如果链表的下个位置已经没有值了,则新建一个Node加入到链表(如果链表长度达到8并且容器大小达到64,则将链表转为红黑树)
3.2.2 如果前面找到节点中的key和当前key相等的Node,将新value替换该Node的旧value值,并将旧的value值作为方法返回值并返回,方法结束(7.7)
3/3 如果新增元素成功,则最后判断是否需要扩容,最后返回null,方法结束(7.8)
-----当新增元素后,元素的数量超过了扩容阈值(容量 * 加载因子)则将HashMap的容量扩充为原来的2倍

2 HashMap的静态常量

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
	// 默认的初始化容量16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
	// 最大容量1073741823
    static final int MAXIMUM_CAPACITY = 1 << 30;
	// 默认加载因子0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
	// 树化阈值
    static final int TREEIFY_THRESHOLD = 8;
	// 取消树化阈值
    static final int UNTREEIFY_THRESHOLD = 6;
	// 最小树化容量
    static final int MIN_TREEIFY_CAPACITY = 64;
}

3 HashMap的内部类

在HashMap中,每一个Node的key,value属性都是map中一条键值对数据

3.1 Node

Node是基本的哈希桶节点,单向链表的数据结构,并实现了Map.Entry接口

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
	
    static class Node<K,V> implements Map.Entry<K,V> {
    	// hash值
        final int hash;
        final K key;
        V value;
        // 后继节点
        Node<K,V> next;
    }
}
3.2 TreeNode

哈希桶节点,红黑树结构(继承了LinkedHashMap.Entry, LinkedHashMap的内部了Entry继承了HashMap的内部类Node,所以TreeNode也是Node的子类)

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
	
    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    	// 父节点
        TreeNode<K,V> parent;  // red-black tree links
        // 左子节点
        TreeNode<K,V> left;
        // 右子节点
        TreeNode<K,V> right;
        // 指向前一个节点
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
    }
    
}

4 HashMap的属性

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
	// 哈希表,第一次使用时初始化,根据需要扩容,每次大小扩容为当前的2倍
    transient Node<K,V>[] table;
	// Map.Entry的缓存Set
    transient Set<Map.Entry<K,V>> entrySet;
	// key-value键值对的条目数
    transient int size;
	// 记录了HashMap结构被修改的次数。该字段用于在HashMap的Collection视图上使迭代器快速失败(fail-fast),可避免并发修改导致的问题
    transient int modCount;
	// 扩容阈值,当元素将要超过这个值时则需要扩容(容量 * 负载系数)
    int threshold;
	// 加载因子
    final float loadFactor;
}

5 HashMap的构造函数

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
	// 1,无仓构造器
	public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    
    // 2,参数为initialCapacity的构造器
    public HashMap(int initialCapacity) {
 		// 调用构造器3
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
	
	// 3,参数为initialCapacity和loadFactor的构造器
	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;
        // 将初始容量设置为大于等于initialCapacity最小的2的幂次方的值
        this.threshold = tableSizeFor(initialCapacity);
    }
}

6 put方法

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
6.1 putVal的形参和返回值

Params:
1,hash – hash for key 通过hashcode重新计算后的hash值
2,key – the key 键
3,value – the value to put 值
4,onlyIfAbsent – if true, don’t change existing value 如果为true,在map中已经存在key的时候将不会覆盖已经存在的key的value
evict – if false, the table is in creation mode. 如果为false,集合将为创建模式。该模式下不允许进行删除操作。这通常发生在哈希表正在进行初始化或者扩容操作时。在这种模式下,插入操作只会发生在桶为空的位置,不会对已有的键值对进行替换或更新。
Returns:
previous value, or null if none 当前key在map中之前已经存在的值,如果之前不存在当前key则返回null

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

Computes key.hashCode() and spreads (XORs) higher bits of hash to lower. Because the table uses power-of-two masking, sets of hashes that vary only in bits above the current mask will always collide. (Among known examples are sets of Float keys holding consecutive whole numbers in small tables.) So we apply a transform that spreads the impact of higher bits downward. There is a tradeoff between speed, utility, and quality of bit-spreading. Because many common sets of hashes are already reasonably distributed (so don’t benefit from spreading), and because we use trees to handle large sets of collisions in bins, we just XOR some shifted bits in the cheapest possible way to reduce systematic lossage, as well as to incorporate impact of the highest bits that would otherwise never be used in index calculations because of table bounds.

计算哈希码和将哈希码的高位向低位进行扩散(XOR操作)过程中。因为哈希表使用的是2的幂次方进行掩码操作,具有相同掩码以上位不同的哈希码集合将会发生碰撞(collision)。为了解决这个问题,采用了一种变换,将高位的影响向下扩散。在速度、效用和比特扩散质量之间存在权衡。由于许多常见的哈希集合已经有了合理的分布(因此不需要扩散),而且我们使用树来处理槽中大量碰撞的情况,因此只需要以最便宜的方式对一些位进行位移和XOR运算,以减少系统性损失,并将原本不会在索引计算中使用的最高位的影响纳入其中。

作用:有效地减少hash冲突的概率,提高HashMap在处理不同key时的性能和效率

7 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)
        	// 如果当前的哈希表未进行初始化,则进行初始化扩容(7.1)
            n = (tab = resize()).length;
        // 计算key的哈希表下标,并判断当前哈希表中该下标的位置有没有值,有没有产生hash碰撞(7.2)
        if ((p = tab[i = (n - 1) & hash]) == null)
       		// 无哈希冲突时,给哈希表通过key计算出的下标位置赋值(7.3)
            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))))
                //  哈希表表下标位置有Node,该Node的key和当前key一致(7.4)
                e = p; // 将该Node赋值给e,在后面会把将当前value替换掉e的value
            else if (p instanceof TreeNode)
            	// 哈希表表下标位置有Node,Node是红黑树类型
                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);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        	// 如果节点数已经达到树化阈值并且集合的容量达到了最小树化容量(treeifyBin方法中会判断),则将链表转化为红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        // 当链表中某个节点的key和当前key一致,则返回当前Node
                        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;
            }
        }
        // 修改次数加一操作,当迭代器调用next方法的时候会判断迭代器创建时的modCount和当前modCount是否相等,如果不相等的话说明集合已经添加或删除过元素了,则会抛出ConcurrentModificationException异常
        ++modCount;
        if (++size > threshold)
        	// 如果添加元素之后,集合中元素个数将超过扩容阈值(容量 * 负载系数),则进行扩容(7.1)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
7.1 扩容resize方法

扩容概述:
1,如果当前的哈希表未进行初始化,首次put元素时会进行扩容
1.1,如果构建HashMap的时候没有设置容量,则会扩量到默认值16
1.2,如果构建HashMap的时候设置了容量,则会在构造函数中计算初始化扩容值并赋值给扩容阈值threshold
计算的返回结果为一个大于等于且最接近我们所设置的容量值的,并且是2的幂次方的值

例1:我们设置HashMap容量为0,那么这个扩容阈值要大于等于0,且是2的幂次方,且最接近0,那这个值就是2的0次方1
例2:我们设置HashMap容量为22,那么这个扩容阈值要大于等于2,且是2的幂次方,且最接近22,那这个值就是2的5次方32

resize方法会将容量扩容到计算得到的threshold
2,新增元素后,HashMap中的元素超过了扩容阈值会进行扩容
3,每次扩容都会将容量扩大到原来的2倍

7.2 计算key的哈希表下标,判断该位置是否已经有值
Node<K,V>[] tab; Node<K,V> p; int n, i;
tab = table;
n = tab.length)
i = (n - 1) & hash

通过源码可以看出计算key在哈希表下标的方式为将哈希表数组长度减一之后和hash值进行按位与运算
因为哈希表长度会是2的幂次方所以在长度减一之后二进制所有位的值都会是1,见下表

十进制二进制长度-1后的二进制
410011
16100001111
3210000011111

按位与运算:对两个二进制数相应的位进行比较并生成一个新的二进制数。如果两个相应的位都是1,则结果为1。
否则,结果为0。
分析之后,可以得出当二进制数的每一位都是1的时候,和它按位与运算能得出的所有结果是从0到它当前的值,(也就是0 到 n-1,正好是数组的有效下标)

(p = tab[i = (n - 1) & hash]) == null

判断当前哈希表该下标位置是否有值,如果有则是发生了哈希碰撞

7.3 当前下标位置没有Node,直接添加元素
if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
    // Create a regular (non-tree) node
    Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
        return new Node<>(hash, key, value, next);
    }

创建一个新的Node节点,并将其放在哈希表中前面计算得到的下标位置

7.4 哈希表表下标位置有Node,该Node的key和当前key一致
			Node<K,V> e; K k;
			if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
				e = p;
			// ....
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }

Node为哈希表当前下标位置的节点
1,Node的hash值和当前key计算出的hash值一致
2,Node的key值等于当前key的值
同时满足以上两个条件,接下来就会将当前的value替换掉Node中的value

7.5 哈希表表下标位置有Node,该Node是红黑树类型
		else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                       int h, K k, V v) {
            Class<?> kc = null;
            boolean searched = false;
            // 获取根节点
            TreeNode<K,V> root = (parent != null) ? root() : this;
            // 遍历红黑树
            for (TreeNode<K,V> p = root;;) {
                int dir, ph; K pk;
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
               	// 找到和key一致的Node则将它返回
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
                // 如果key的类型不支持compare比较,或者compare的结果相等但是两个值不相等,则会继续深入到当前节点的左子节点和右子节点中进行递归查找,如果找到和key一致的Node则将它返回
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0) {
                    if (!searched) {
                        TreeNode<K,V> q, ch;
                        searched = true;
                        if (((ch = p.left) != null &&
                             (q = ch.find(h, k, kc)) != null) ||
                            ((ch = p.right) != null &&
                             (q = ch.find(h, k, kc)) != null))
                            return q;
                    }
                    dir = tieBreakOrder(k, pk);
                }
				// 如果查找方向的位置为null,说明树中没有key的值,则新建一个节点添加到红黑树,最后返回null,因为已经没有需要替换value的node了
                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    Node<K,V> xpn = xp.next;
                    TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    xp.next = x;
                    x.parent = x.prev = xp;
                    if (xpn != null)
                        ((TreeNode<K,V>)xpn).prev = x;
                    moveRootToFront(tab, balanceInsertion(root, x));
                    return null;
                }
            }
        }

该方法在遍历中主要做两件事情:
1,发现存在节点中的key和当前key相等的Node,将它返回,后面将替换该Node中的value值
2,红黑树中如果不存在节点中的key和当前key相等的Node,则新建一个节点,插入到红黑树中

7.6 当前下标位置有Node,Node是链表
            else {
            	// 依次遍历哈希表下标位置的链表
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                    	// 如果链表的下个位置已经没有值了,则新建一个Node加入到链表
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        	// 如果节点数已经达到树化阈值并且集合的容量达到了最小树化容量(treeifyBin方法中会判断),则将链表转化为红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        // 当链表中某个节点的key和当前key一致,则返回当前Node
                        break;
                    p = e;
                }
            }
7.7 替换集合中相同key节点的value
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }

e属性就是前面判断得到节点中的key与当前key一致的Node,方法将新value值替换该Node的旧value值,并将旧的value值作为方法返回值并返回

7.8 新增元素成功后判断是否需要扩容
        // 修改次数加一操作,当迭代器调用next方法的时候会判断迭代器创建时的modCount和当前modCount是否相等,如果不相等的话说明集合已经添加或删除过元素了,则会抛出ConcurrentModificationException异常
        ++modCount;
        if (++size > threshold)
        	// 如果添加元素之后,集合中元素个数将超过扩容阈值(容量 * 负载系数),则进行扩容(7.1)
            resize();
        afterNodeInsertion(evict);
        return null;

如果添加元素之后的值大于扩容阈值,则调用resize方法进行扩容,扩容源码见7.1
因为新增元素不存在被覆盖的旧值,所以返回null

  • 22
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值