一、整体结构
HashMap是Java集合框架中一种重要的数据结构,它按照Key-Value键值对的形式存储数据,即每一个值(Value)都由一个唯一的键(Key)与之对应。HashMap的工作原理是使用"拉链法"来解决哈希冲突的问题。
整个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;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子默认值
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//桶上的链表长度大于等于8时,链表转化成红黑树
static final int TREEIFY_THRESHOLD = 8;
//桶上的红黑树大小小于等于6时,红黑树转化成链表
static final int UNTREEIFY_THRESHOLD = 6;
//当数组容量大于 64 时,链表才会转化成红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
//记录迭代过程中 HashMap 结构是否发生变化,如果有变化,迭代时会 fail-fast
transient int modCount;
//HashMap 的实际大小,可能不准(因为当你拿到这个值的时候,可能又发生了变化)
transient int size;
//存放数据的数组
transient Node<K,V>[] table;
// 扩容的门槛,有两种情况
// 如果初始化时,给定数组大小的话,通过 tableSizeFor 方法计算,数组大小永远接近于 2 的幂次方,比如你给定初始化大小 19,实际上初始化大小为 32,为 2 的 5 次方。
// 如果是通过 resize 方法进行扩容,大小 = 数组容量 * 0.75
int threshold;
// 内部使用静态内部类 Node<K,V> 来表示存储的键值对
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
// 省略构造函数和方法
}
//红黑树的节点
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;
}
二、Java JDK 1.8中Node内部类的代码实现
Node
是HashMap
中最核心的内部类,它实现了Map.Entry
接口,用于存储键值对数据。下面是Node
类的主要属性及其作用:
-
hash
这是键值对中键的哈希值,在存储和查找时用于计算该键值对在数组中的存储位置。哈希值是final类型,在创建Node时就已经计算好并存储,以提高效率。 -
key
这是键值对中的键,也是final类型,一旦创建便不可修改。 -
value
这是键值对中的值,不是final类型,可以修改。 -
next
这是一个指向下一个节点的引用,用于解决哈希冲突时构建链表结构。
当插入新的键值对时,HashMap
会先根据键的哈希值计算该键值对应存储在数组的哪个位置。如果该位置没有数据,就直接将新的Node
存储在该位置;如果该位置已有数据,就需要构建链表结构,将新的Node
插入到链表的末尾。
在查找键值对时,HashMap
同样会先计算键的哈希值,找到对应的数组位置。如果该位置为空,则直接返回null;如果有数据,则需要遍历该位置上的链表,查找是否有键相同的Node
。
当链表长度过长时(默认阈值为8),桶大小大于64,为了提高查找效率,HashMap
会将链表转化为红黑树结构。当红黑树节点个数较少时(默认阈值为6),则会重新转化为链表结构。
三、Java JDK 1.8中TreeNode内部类的代码实现
TreeNode
是HashMap
中用于构建红黑树数据结构的内部类。它继承自LinkedHashMap.Entry
类,因为HashMap
在树化时会复用LinkedHashMap
中的双向链表节点,以方便地调整树的结构。
下面是TreeNode
类的主要属性及其作用:
-
parent
、left
、right
这三个属性分别代表该节点的父节点、左子节点和右子节点,用于构建红黑树的层级关系结构。 -
prev
这个属性继承自LinkedHashMap.Entry
,用于维护双向链表的前驱指针。当删除节点时,可以方便地将其从双向链表中移除。 -
red
这是一个布尔类型的标志位,用于标记当前节点是红色节点还是黑色节点。红黑树通过这种方式,来维护自身的平衡性,从而保证了树的查找、插入和删除操作的时间复杂度为O(logN)
。
TreeNode
继承自LinkedHashMap.Entry
类,因此它也具备了键值对的存储功能。但是,TreeNode
的主要作用是作为红黑树的节点,用于构建一个平衡的搜索树结构。
三、HashMap的基本工作流程
-
根据键值对的键调用散列函数得到一个哈希值。
-
将哈希值与(桶数组长度-1)相与运算得到存储在桶数组中的具体位置,即数组下标。
-
如果该位置的桶为空,则直接将键值对存储在该位置;如果不为空(哈希冲突),则以链表形式将新的键值对存储在链表后。
-
如果存在过多的哈希冲突,会触发树化(当单个链表长度达到8时),并且数组大小大于64,将链表转换为红黑树,以优化查询效率。当链表长度小于6时,红黑树转链表
-
当HashMap中存储的键值对达到一定数量(超过加载因子*初始容量)时,会触发重新散列,扩容并重新存储所有键值对。
四、初始化对象
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);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
Java JDK 1.8中HashMap
的这几个构造函数
public HashMap(int initialCapacity, float loadFactor)
这个构造函数允许用户在创建HashMap
时指定初始容量和加载因子。它首先会检查传入的参数是否合法:初始容量不能为负数,加载因子必须是正数。
如果初始容量过大,会被重置为MAXIMUM_CAPACITY
(2^30)。然后,它会将加载因子赋值给loadFactor
字段,并根据指定的初始容量,计算出实际的阈值(threshold
)并赋值。其中tableSizeFor()
方法会找到最小的大于等于初始容量的2的幂次方值作为实际容量。
public HashMap(int initialCapacity)
这个构造函数只允许指定初始容量,加载因子使用默认值DEFAULT_LOAD_FACTOR
(0.75)。它调用了第一个构造函数,将加载因子设置为默认值。
public HashMap()
这是无参构造函数,创建一个默认的HashMap
实例。初始容量是16,加载因子是0.75。所有其他字段都使用默认值。
public HashMap(Map<? extends K, ? extends V> m)
这个构造函数允许用户将一个现有的Map
初始化到新创建的HashMap
中。它首先将加载因子设置为默认值0.75,然后调用putMapEntries()
方法,将传入Map
中的所有键值对插入到新的HashMap
中。
如果对性能要求不高,使用无参构造函数或只指定初始容量的构造函数就足够了。如果对性能有较高要求,可以适当增加初始容量和调整加载因子,以减少频繁的扩容和重新散列操作。
五、put()源码
public V put(K key, V value) {
return putVal(hash(key), key, value, 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;
// 如果哈希桶数组为空或长度为0,先初始化或者扩容
if ((tab = table) == null || (n = tab.length) == 0)
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;
// hash和值相同,如果当前节点就是要put的key,则直接覆盖value值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果当前节点是TreeNode红黑树类型,调用红黑树的putTreeVal方法
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 否则,就是链表操作
for (int binCount = 0; ; ++binCount) {
// 如果链表遍历到尾部,仍未找到相同key的节点,就创建一个新节点加入到链表尾部
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;
}
// 如果找到了相同key的节点,则终止循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果找到了相同key的节点
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 如果允许覆盖,就替换value值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 如果是新键值对,modCount加1
++modCount;
// 如果元素个数超过阈值,则扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
// 返回null,代表put成功
return null;
}
-
首先定义了几个变量,分别是
table
数组、节点p
、数组长度n
和索引i
。 -
检查
table
数组是否为空或长度为0,如果是则调用resize()
方法重新分配内存。 -
根据键的哈希值计算出该键值对在
table
数组中的存储位置索引i
。如果该位置为空,则直接创建一个新的节点存储该键值对。 -
如果该位置不为空,意味着发生了哈希冲突,需要遍历冲突链表或红黑树:
a) 如果遍历到的节点的键与要插入的键相同,则直接覆盖该节点的值。
b) 如果遍历到的节点是红黑树节点,则调用树的插入方法
putTreeVal
插入新的键值对。c) 如果遍历到的节点是链表节点,则沿着链表继续遍历,直到链表尾部。
i. 如果遍历到链表尾部都没有找到相同的键,则在链表尾部创建一个新的节点。
ii. 如果链表长度超过阈值(默认为8),则将链表转化为红黑树。
-
如果找到了相同的键,则根据
onlyIfAbsent
参数决定是否覆盖值。如果覆盖,则返回原来的值。 -
如果是一个新的键值对,则增加
size
计数和modCount
计数。如果size
超过阈值,则调用resize
方法扩容。 -
最后返回
null
,表示put操作成功执行。
核心是通过哈希值找到对应的存储位置,然后处理哈希冲突。通过链表和红黑树的数据结构,HashMap
可以高效地存储和查找键值对。
treeifyBin
树化方法
final void treeifyBin(Node<K, V>[] tab, int hash) {
// 定义局部变量
int n, index;
Node<K, V> e;
// 如果哈希桶数组为空或长度小于树化阈值(64),则先进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 否则,获取该哈希值对应的链表头节点
else if ((e = tab[index = (n - 1) & hash]) != null) {
// 定义树的头节点和尾节点
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);
}
}
将特定位置上的链表节点转化为红黑树节点,以提高查找效率
-
首先判断哈希桶数组
tab
是否为空或长度小于树化阈值MIN_TREEIFY_CAPACITY
(64)。如果是,则先进行扩容操作,以确保数组足够大。 -
获取该哈希值对应的链表头节点
e
。 -
定义树的头节点
hd
和尾节点tl
。 -
遍历链表,将每个节点
e
转化为TreeNode
节点,并用prev
和next
指针链接起来,组成一个双向链表。同时维护hd
和tl
指针指向头节点和尾节点。 -
将树的头节点
hd
赋值给哈希桶数组tab
的对应位置。 -
如果
hd
不为空,则调用treeify
方法对树进行树化操作,构建一颗红黑树。
核心当链表长度大于等于 8 时,此时的链表就会转化成红黑树,此方法有一个判断,当链表长度大于等于 8,并且整个数组大小大于 64 时,才会转成红黑树,当数组大小小于 64 时,只会触发扩容resize
(),不会转化成红黑树。由于红黑树的特性,查找、插入和删除操作的时间复杂度为O(log n)
。相比链表的O(n)
时间复杂度,红黑树的查找效率更高。
treeify
方法构建一颗红黑树
final void treeify(Node<K, V>[] tab) {
// 定义根节点
TreeNode<K, V> root = null;
// 遍历双向链表,将每个节点插入到红黑树中
for (TreeNode<K, V> x = this, next; x != null; x = next) {
next = (TreeNode<K, V>) x.next;
// 将当前节点的左右子节点先设置为null
x.left = x.right = null;
// 如果是第一个节点,则将该节点设置为根节点
if (root == null) {
x.parent = null;
x.red = false; // 根节点设置为黑色
root = x;
}
// 否则,按照红黑树的规则插入该节点
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
// 从根节点开始遍历,查找插入位置
for (TreeNode<K, V> p = root; ; ) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1; // 小于当前节点,查找左子树
else if (ph < h)
dir = 1; // 大于当前节点,查找右子树
// 如果哈希值相同,需要比较实际的键值大小
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk); // 如果键值相等,则按照系统顺序比较
TreeNode<K, V> xp = p;
// 如果找到了插入位置,则插入该节点,并调整树的平衡
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x); // 调整树的平衡
break;
}
}
}
}
// 将根节点移动到链表头部
moveRootToFront(tab, root);
}
将双向链表转化为红黑树,以提高查找效率
-
定义根节点
root
。 -
遍历双向链表,对于每个节点
x
:- 将
x
的左右子节点先设置为null
。 - 如果
root
为空,则将x
设置为根节点。 - 否则,按照红黑树的规则插入该节点:
- 计算节点的哈希值和键值。
- 从根节点开始遍历,比较哈希值和键值,查找插入位置。
- 找到合适的插入位置后,插入该节点,并调用
balanceInsertion
方法调整树的平衡。
- 将
-
遍历结束后,调用
moveRootToFront
方法,将根节点移动到双向链表的头部。
在插入节点时,会严格按照红黑树的规则进行操作,包括比较键值、查找插入位置、调整树的平衡等,以确保红黑树的性质。
六、get()源码
public V get(Object key) {
Node<K, V> e;
//key首先要获取hash
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) {
// 如果首个节点就是要查找的节点,直接返回
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);
// 链表查找的关键代码
// 如果当前节点 hash 等于 key 的 hash,并且 equals 相等,当前节点就是我们要找的节点
// 当 hash 冲突时,同一个 hash 值上是一个链表的时候,我们是通过 equals 方法来比较 key 是否相等的
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e; // 找到目标节点,返回
// 否则,把当前节点的下一个节点拿出来继续寻找
} while ((e = e.next) != null);
}
}
// 如果遍历完都没找到,返回null
return null;
}
getf方法根据给定的哈希值和键,在HashMap
中查找对应的节点
- 根据 hash 算法定位数组的索引位置,equals 判断当前节点是否是我们需要寻找的 key,是的话直接返回,不是的话往下。
- 判断当前节点有无 next 节点,有的话判断是链表类型,还是红黑树类型。
- 分别走链表和红黑树不同类型的查找方法。
红黑树getTreeNode
查找源码
/**
* Calls find for root node.
*/
final TreeNode<K,V> getTreeNode(int h, Object k) {
// 获取当前节点的根节点
return ((parent != null) ? root() : this).find(h, k, null);
}
return ((parent != null) ? root() : this).find(h, k, null);
- 它首先检查当前节点(
this
)是否有父节点(parent != null
)。- 如果有父节点,则调用
root()
方法获取整个树的根节点。 - 如果没有父节点,说明当前节点就是根节点。
- 如果有父节点,则调用
- 然后,无论是获取到的根节点还是当前节点本身,都会调用其
find(h, k, null)
方法。find
方法用于在红黑树中查找具有给定哈希值h
和键k
的节点。- 第三个参数
null
表示从根节点开始查找,而不是从指定的节点开始查找。
- 它首先检查当前节点(
获取红黑树的根节点,然后调用根节点的find
方法在整个树中查找具有指定哈希值和键的节点。
红黑树查找实现细节
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
// 从当前节点开始查找
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
// 如果当前节点的哈希值大于要查找的哈希值,则去左子树查找
if ((ph = p.hash) > h)
p = pl;
// 如果当前节点的哈希值小于要查找的哈希值,则去右子树查找
else if (ph < h)
p = pr;
// 如果当前节点的哈希值等于要查找的哈希值
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
// 如果当前节点的键也相等,则返回当前节点
return p;
// 如果当前节点的左子树为空,则去右子树查找
else if (pl == null)
p = pr;
// 如果当前节点的右子树为空,则去左子树查找
else if (pr == null)
p = pl;
// 如果能够进行键的比较操作
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
// 根据键的大小关系,决定去左子树还是右子树查找
p = (dir < 0) ? pl : pr;
// 如果去右子树查找,并且找到了结果,则返回结果
else if ((q = pr.find(h, k, kc)) != null)
return q;
// 否则,去左子树继续查找
else
p = pl;
// 如果p为null,则说明整棵树都找不到目标节点,结束循环
} while (p != null);
// 如果循环结束还没有找到,则返回null
return null;
}
红黑树中查找具有指定哈希值和键的节点的功能
-
从当前节点
p
开始查找。 -
进入一个
do-while
循环,循环条件是p != null
。 -
在循环中,首先获取当前节点的哈希值
ph
、键pk
以及左右子节点pl
和pr
。 -
根据当前节点的哈希值与目标哈希值的大小关系,决定是去左子树还是右子树继续查找。
-
如果当前节点的哈希值等于目标哈希值,则比较键是否相等。如果相等,则返回当前节点。
-
如果当前节点的左子树或右子树为空,则根据情况决定去另一侧子树查找。
-
如果能够进行键的比较操作,则根据键的大小关系决定去左子树还是右子树查找。
-
如果去右子树查找并且找到了结果,则直接返回结果。否则,去左子树继续查找。
-
如果循环结束还没有找到目标节点,则返回
null
。
利用了红黑树的二叉查找树特性,通过比较哈希值和键的大小关系,高效地在树中查找目标节点。如果找到了目标节点,则直接返回;如果没有找到,则返回null
。
七、remove()源码
对removeNode
方法分析
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;
// 如果哈希桶数组不为空,并且该哈希值对应的首个节点也不为空
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;
// 如果首个节点就是要删除的节点,将node赋值为首个节点
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);
}
}
// 如果找到了要删除的节点,并且值也匹配(若matchValue为true)
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 如果是树节点,则调用树的删除方法
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 如果是首个节点,则将首个节点设置为其下一个节点
else if (node == p)
tab[index] = node.next;
// 否则,将当前节点的下一个节点设置为要删除节点的下一个节点
else
p.next = node.next;
// 修改modCount和size
++modCount;
--size;
// 删除节点后的回调方法
afterNodeRemoval(node);
// 返回被删除的节点
return node;
}
}
// 如果没有找到要删除的节点,返回null
return null;
}
-
首先定义了一些局部变量,包括哈希桶数组
tab
、首个节点p
、数组长度n
和索引index
。 -
检查哈希桶数组是否为空,长度是否大于0,并获取该哈希值对应的首个节点
p
。如果这些条件不满足,直接返回null
。 -
如果
p
不为空,则查找要删除的节点:- 如果
p
就是要删除的节点,则将node
赋值为p
。 - 如果
p
不是要删除的节点,则继续遍历后续节点:- 如果
p
是树节点,则调用getTreeNode
方法在树中查找要删除的节点。 - 如果
p
是链表节点,则遍历链表,查找要删除的节点。
- 如果
- 如果
-
如果找到了要删除的节点
node
,并且值也匹配(若matchValue
为true
):- 如果
node
是树节点,则调用树的removeTreeNode
方法删除该节点。 - 如果
node
是首个节点,则将首个节点设置为其下一个节点。 - 否则,将当前节点的下一个节点设置为要删除节点的下一个节点。
- 更新
modCount
和size
计数。 - 调用
afterNodeRemoval
方法,执行删除节点后的回调操作。 - 返回被删除的节点。
- 如果
-
如果没有找到要删除的节点,则返回
null
。
八、如何扩容?
-
触发扩容的条件
HashMap会在两种情况下进行扩容:
- 当HashMap中的元素个数超过阈值时,阈值计算方式是:阈值 = 容量 x 加载因子
- 在插入新元素时,如果发生了较多的哈希冲突,导致有一个桶位置上的单向链表长度大于等于8时
-
扩容的准备
- 首先计算新的容量和新的阈值,新容量是原容量的2倍,新阈值是新容量乘以加载因子
- 创建一个长度为新容量的新数组
-
重新放置元素
- 遍历原数组中所有节点
- 重新计算每个节点在新数组中的位置索引
- 将节点放置在新数组的相应位置
- 如果该位置为空,则直接存放
- 如果该位置已有节点,则形成一个新的单向链表
-
处理单向链表过长
- 如果在放置节点时,某个位置上形成的单向链表长度大于等于8,则需要进一步判断
- 只有在数组长度大于等于64的情况下,才将该单向链表转化为红黑树,以提高查找性能
- 如果数组长度小于64,则即使链表过长,也不会树化,而是等待下一次扩容再处理
-
扩容完成
- 经过上述操作,所有节点都已重新计算位置并放置在新数组中
- 最后,将新数组赋值给HashMap的底层数组引用
HashMap在容量不足时能够扩大容量,减少哈希冲突的概率;同时也处理了单向链表过长的情况,在适当的时候通过树化提高查找性能。