HashMap的历史
HashMap最早是在jdk1.2中开始出现的,一直到jdk1.7一直没有太大的变化。
但是到了jdk1.8突然进行了一个很大的改动。其中一个最显著的改动就是:
之前jdk1.7的存储结构是数组+链表,到了jdk1.8变成了数组+链表+红黑树。
另外,HashMap是非线程安全的,进行增删改操作的时候,是不能保证数据的一致性的。

红黑树是一个自平衡的二叉查找树,也就是说红黑树的查找效率是非常的高,查找效率会从链表的o(n)降低为o(logn)。
HashMap名词介绍
名称 | 用途 |
---|---|
initialCapacity | HashMap 初始容量 |
loadFactor | 负载因子 |
threshold | 当前 HashMap 所能容纳键值对数量的最大值,超过这个值,则需扩容 |
负载因子(loadFactor)
对于 HashMap 来说,负载因子是一个很重要的参数,该参数反应了 HashMap 桶数组的使用情况。
通过调节负载因子,可使 HashMap 时间和空间复杂度上有不同的表现。
当我们调低负载因子时,HashMap 所能容纳的键值对数量变少。
扩容时,重新将键值对存储新的桶数组里,键的键之间产生的碰撞会下降,链表长度变短。此时,HashMap 的增删改查等操作的效率将会变高,这里是典型的拿空间换时间。
相反,如果增加负载因子(负载因子可以大于1),HashMap 所能容纳的键值对数量变多,空间利用率高,但碰撞率也高。这意味着链表长度变长,效率也随之降低,这种情况是拿时间换空间。
至于负载因子怎么调节,这个看使用场景了。一般情况下,我们用默认值就可以了。
Node[] table
即哈希桶数组 ,Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。
Node[] table的初始化长度length(默认值是16),
Load factor为负载因子(默认值是0.75),
threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。
threshold = length * Load factor。
也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。
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() { 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; } }
源码解析
初始化 threshold 的方法
/** * Returns a power of two size for the given target capacity. */ 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; }
总结起来就一句话:找到大于或等于 cap 的最小2的幂。
put(K key, V value)
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
也就是说,put方法其实调用的是putVal方法。putVal方法有5个参数: (1)第一个参数hash:调用了hash方法计算hash值 (2)第二个参数key:就是我们传入的key值,也就是例子中的张三 (3)第三个参数value:就是我们传入的value值,也就是例子中的20 (4)第四个参数onlyIfAbsent:也就是当键相同时,不修改已存在的值 (5)第五个参数evict :如果为false,那么数组就处于创建模式中,所以一般为true。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; //定义的数组 Node<K,V> p; //当前插入的节点 int n, i; //第一部分 //如果table尚未初始化,则此处进行初始化数组,并赋值初始容量,重新计算阈值 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //第二部分 //判断hash值,不冲突,new一个Node插入进去 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //第三部分 //处理冲突,如果通过hash找到的位置有数据,发生碰撞 else { Node<K,V> e; K k; //第三部分--A //p是当前节点,如果需要插入的key和当前hash值指定下标的key一样,一样的直接替换旧值e if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //第三部分--B //如果此时桶中数据类型为 treeNode,使用红黑树进行插入 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //第三部分--C //此时桶中数据类型为链表 else { //看这里的 .next,如果是数组,就会进去遍历 //没有 就newNode //化树的阈值,TREEIFY_THRESHOLD,所以不存在并且在链表末尾插入元素的时候 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; } //如果链表中有新插入的节点位置数据不为空,则此时e 赋值为节点的值,跳出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //经过上面的循环后,如果e不为空,则说明上面插入的值已经存在于当前的hashMap中, //那么更新指定位置的键值对 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; }
代码看,put方法分为三种情况:
- table尚未初始化,对数据进行初始化
- table已经初始化,且通过hash算法找到下标所在的位置数据为空,直接将数据存放到指定位置
- table已经初始化,且通过hash算法找到下标所在的位置数据不为空,发生hash冲突(碰撞),发生碰撞后,会执行以下操作:
- 判断插入的key如果等于当前位置的key的话,将 e 指向该键值对
- 如果此时桶中数据类型为 treeNode,使用红黑树进行插入
- 如果是链表,则进行循环判断, 如果链表中包含该节点,跳出循环,如果链表中不包含该节点,则把该节点插入到链表末尾,同时,如果链表长度超过树化阈值(TREEIFY_THRESHOLD)且table容量超过最小树化容量(MIN_TREEIFY_CAPACITY),则进行链表转红黑树(由于table容量越小,越容易发生hash冲突,因此在table容量<MIN_TREEIFY_CAPACITY 的时候,如果链表长度>TREEIFY_THRESHOLD,会优先选择扩容,否则会进行链表转红黑树操作)
扩容前的小知识
怎么计算Hash值?
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
与16异或的位运算。
在数据结构中,我们处理hash冲突常使用的方法有:开发定址法、再哈希法、链地址法、建立公共溢出区。而hashMap中处理hash冲突的方法就是链地址法。
“链”,很容易想到,因为常用删除修改等操作,所以效率更高。
initialCapacity初始容量(你输入的最接近2的N次幂的数字),loadFactor负载因子(0.75,泊松分布)。
/* ---------------- Public operations -------------- */ /** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and load factor. * * @param initialCapacity the initial capacity * @param loadFactor the load factor * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive */ 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); } /** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and the default load factor (0.75). * * @param initialCapacity the initial capacity. * @throws IllegalArgumentException if the initial capacity is negative. */ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } /** * Constructs an empty <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } /** * Constructs a new <tt>HashMap</tt> with the same mappings as the * specified <tt>Map</tt>. The <tt>HashMap</tt> is created with * default load factor (0.75) and an initial capacity sufficient to * hold the mappings in the specified <tt>Map</tt>. * * @param m the map whose mappings are to be placed in this map * @throws NullPointerException if the specified map is null */ public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
扩容 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 */ //初始化或加倍表大小。如果为空,则根据字段阈值中持有的初始容量目标进行分配。否则,因为我们使用的是 2 的幂扩展,所以每个 bin 中的元素必须保持相同的索引,或者在新表中以 2 的幂的偏移量移动。 final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; //threshold 临界点 int newCap, newThr = 0; //第一部分,扩容 //如果超过了直接将阈值设置为整数的最大值,没有,则,通过移位操作扩大2倍。 if (oldCap > 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 newCap = oldThr; //threshold 和 table 皆未初始化情况,此处即为首次进行初始化 //也就在此处解释了构造方法中没有对threshold 和 初始容量进行赋值的问题 else { // zero initial threshold signifies using defaults //如果阈值为零,表示使用默认的初始化值 //这种情况在调用无参构造的时候会出现,此时使用默认的容量和阈值 newCap = DEFAULT_INITIAL_CAPACITY; //此处阈值即为 threshold=initialCapacity*loadFactor newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // newThr 为 0 时,按阈值计算公式进行计算,容量*负载因子 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //更新阈值 threshold = newThr; //更新数组桶 @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; //第三部分 //将旧的数据迁移到新的数组 //如果之前的数组桶里面已经存在数据,由于table容量发生变化,hash值也会发生变化,需要重新计算下标 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) //直接将数据存放到新计算的hash值下标下 newTab[e.hash & (newCap - 1)] = e; //如果是TreeNode数据结构 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //对于链表,数据结构 else { // preserve order //如果是链表,重新计算hash值,根据新的下标重新分组 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; }
resize分为以下几步:
- 首先先判断当前table是否进行过初始化,如果没有进行过初始化,此处就解决了调用无参构造方法时候,threshold和initialCapacity 未初始化的问题,如果已经初始化过了,则进行扩容,容量为原来的二倍
- 扩容后创建新的table,并对所有的数据进行遍历
- 如果新计算的位置数据为空,则直接插入
- 如果新计算的位置为链表,则通过hash算法重新计算下标,对链表进行分组
- 如果是红黑树,则需要进行拆分操作
get方法,查找
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //根据hash算法找到对应位置的第一个数据,如果是指定的key,则直接返回 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { //如果该节点为红黑树,则通过树进行查找 if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); //如果该节点是链表,则遍历查找到数据 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
hash源码
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
这段代码叫做扰动函数,也是hashMap中的hash运算,主要分为下面几步:
- key.hashCode(),获取key的hashCode值,如果不进行重写的话返回的是根据内存地址得到的一个int值
- key.hashCode() 获取到的hashcode无符号右移16位并和元hashCode进行^ ,这样做的目的是为了让高位与低进行混合,让两者都参与运算,以便让hash值分布更加均匀
取模运算 (n - 1) & hash
在hashMap的代码中,在很多地方都会看到类似的代码:
first = tab[(n - 1) & hash])
hash算法中,为了使元素分布的更加均匀,很多都会使用取模运算,
在hashMap中并没有使用hash%n这样进行取模运算,而是使用(n - 1) & hash进行代替,
原因是在计算机中,&的效率要远高于%。
需要注意的是, 只有容量为2的n次幂的时候,(n - 1) & hash 才能等效hash%n, 这也是hashMap 初始化初始容量时,无论传入任何值,都会通过tableSizeFor(int cap) 方法转化成2的n次幂的原因, 这种巧妙的设计真的很令人惊叹。
有兴趣的可以看一下一位大佬写的文章:由HashMap哈希算法引出的求余%和与运算&转换问题
remove方法,删除
public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; //根据key和key的hash值,查找到对应的元素 if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } //如果查找的了元素node,移除即可 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { //如果是TreeNode,通过树进行移除 if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); //如果是第一个节点,移除第一个节点,将index下标的位置指向第二个节点 else if (node == p) tab[index] = node.next; else //如果不是链表的第一个节点,则移除该节点 p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }
链表树化
static final int TREEIFY_THRESHOLD = 8; /** * 当桶数组容量小于该值时,优先进行扩容,而不是树化 */ static final int MIN_TREEIFY_CAPACITY = 64; 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); } } /** * 将普通节点链表转换成树形节点链表 */ final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; // 桶数组容量小于 MIN_TREEIFY_CAPACITY,优先进行扩容而不是树化 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { // hd 为头节点(head),tl 为尾节点(tail) 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) // 将树形链表转换成红黑树 hd.treeify(tab); } } TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) { return new TreeNode<>(p.hash, p.key, p.value, next); }
在扩容过程中,树化要满足两个条件:
- 链表长度大于等于 TREEIFY_THRESHOLD
- 桶数组容量大于等于 MIN_TREEIFY_CAPACITY
红黑树链化与拆分
链化
final Node<K,V> untreeify(HashMap<K,V> map) { Node<K,V> hd = null, tl = null; // 遍历 TreeNode 链表,并用 Node 替换 for (Node<K,V> q = this; q != null; q = q.next) { // 替换节点类型 Node<K,V> p = map.replacementNode(q, null); if (tl == null) hd = p; else tl.next = p; tl = p; } return hd; } Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) { return new Node<>(p.hash, p.key, p.value, next); }
拆分
// 红黑树转链表阈值 static final int UNTREEIFY_THRESHOLD = 6; final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) { TreeNode<K,V> b = this; // Relink into lo and hi lists, preserving order TreeNode<K,V> loHead = null, loTail = null; TreeNode<K,V> hiHead = null, hiTail = null; int lc = 0, hc = 0; /* * 红黑树节点仍然保留了 next 引用,故仍可以按链表方式遍历红黑树。 * 下面的循环是对红黑树节点进行分组,与上面类似 */ for (TreeNode<K,V> e = b, next; e != null; e = next) { next = (TreeNode<K,V>)e.next; e.next = null; if ((e.hash & bit) == 0) { if ((e.prev = loTail) == null) loHead = e; else loTail.next = e; loTail = e; ++lc; } else { if ((e.prev = hiTail) == null) hiHead = e; else hiTail.next = e; hiTail = e; ++hc; } } if (loHead != null) { // 如果 loHead 不为空,且链表长度小于等于 6,则将红黑树转成链表 if (lc <= UNTREEIFY_THRESHOLD) tab[index] = loHead.untreeify(map); else { tab[index] = loHead; /* * hiHead == null 时,表明扩容后, * 所有节点仍在原位置,树结构不变,无需重新树化 */ if (hiHead != null) loHead.treeify(tab); } } // 与上面类似 if (hiHead != null) { if (hc <= UNTREEIFY_THRESHOLD) tab[index + bit] = hiHead.untreeify(map); else { tab[index + bit] = hiHead; if (loHead != null) hiHead.treeify(tab); } } }
在将普通链表转成红黑树时,HashMap 通过两个额外的引用 next 和 prev 保留了原链表的节点顺序。
这样再对红黑树进行重新映射时,完全可以按照映射链表的方式进行。这样就避免了将红黑树转成链表后再进行映射,无形中提高了效率。
从源码上可以看得出,重新映射红黑树的逻辑和重新映射链表的逻辑基本一致。不同的地方在于,重新映射后,会将红黑树拆分成两条由 TreeNode 组成的链表。
如果链表长度小于 UNTREEIFY_THRESHOLD,则将链表转换成普通链表。否则根据条件重新将 TreeNode 链表树化。
被 transient 所修饰 table 变量
其实这个关键字的作用很好理解,
就是简单的一句话:将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会被序列化。
transient Node<K,V>[] table; /** * Holds cached entrySet(). Note that AbstractMap fields are used * for keySet() and values(). */ transient Set<Map.Entry<K,V>> entrySet; /** * The number of key-value mappings contained in this map. */ transient int size; /** * The number of times this HashMap has been structurally modified * Structural modifications are those that change the number of mappings in * the HashMap or otherwise modify its internal structure (e.g., * rehash). This field is used to make iterators on Collection-views of * the HashMap fail-fast. (See ConcurrentModificationException). */ transient int modCount;
HashMap 并没有使用默认的序列化机制,而是通过实现
readObject/writeObject
两个方法自定义了序列化的内容。
序列化 talbe 存在着两个问题:
- table 多数情况下是无法被存满的,序列化未使用的部分,浪费空间
- 同一个键值对在不同 JVM 下,所处的桶位置可能是不同的,在不同的 JVM 下反序列化 table 可能会发生错误。
第二个问题解释:HashMap 的
get/put/remove
等方法第一步就是根据 hash 找到键所在的桶位置,
但如果键没有覆写 hashCode 方法,计算 hash 时最终调用 Object 中的 hashCode 方法。但 Object 中的 hashCode 方法是 native 型的,不同的 JVM 下,可能会有不同的实现,产生的 hash 可能也是不一样的。也就是说同一个键在不同平台下可能会产生不同的 hash,此时再对在同一个 table 继续操作,就会出现问题。
总结
- HashMap 底层数据结构在JDK1.7之前是由数组+链表组成的,1.8之后又加入了红黑树;链表长度小于8的时候,发生Hash冲突后会增加链表的长度,当链表长度大于8的时候,会先判读数组的容量,如果容量小于64会先扩容(原因是数组容量越小,越容易发生碰撞,因此当容量过小的时候,首先要考虑的是扩容),如果容量大于64,则会将链表转化成红黑树以提升效率。
- hashMap 的容量是2的n次幂,无论在初始化的时候传入的初始容量是多少,最终都会转化成2的n次幂,这样做的原因是为了在取模运算的时候可以使用&运算符,而不是%取余,可以极大的提上效率,同时也降低hash冲突。
- HashMap是非线程安全的,在多线程的操作下会存在异常情况(如形成闭环(1.7),1.8已修复闭环问题,但仍不安全),可以使用HashTable或者ConcurrentHashMap进行代替
HashMap这样的知识点还有很多,一篇文章是说不完的,至于更多的玄机需要我们一起去探索。
参考链接:
HashMap源码分析(jdk1.8,保证你能看懂) - 知乎 (zhihu.com)
Java 8系列之重新认识HashMap - 知乎 (zhihu.com)
有大量时间看这个,特别全: HashMap 源码详细分析(JDK1.8) - SegmentFault 思否
HashMap源码分析 —— 一篇文章搞定HashMap面试 (juejin.cn)