HashMap的构造函数
java
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;
}
HashMap的构造参数可以指定初始容量和负载因子,未指定时使用默认的初始容量和负载因子。默认初始容量为64,负载因子为75%。
当节点使用链表形式时,内部结构如下所示
java
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // key的哈希值
final K key; // key的值
V value; //value的值
Node<K,V> next; // 指向下一个节点
}
当节点使用红黑树时,内部结构如下
java
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;
}
key的哈希值计算方式,由key对象的hashCode()并进行进一步处理方法进行计算
java
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
常用方法
get()方法
java
final Node<K,V> getNode(Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
// 哈希表存在且不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & (hash = hash(key))]) != null) { // 定位到该key在哈希表中的位置
// 如果该位置第一个节点的hash和给出的Key的哈希值相等
// 且这两个对象是一个对象或值相等,直接返回
if (first.hash == hash && ((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;
}
// 红黑树时的查找方法
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;
} while (p != null);
return null;
}
我们看到判断是不是目标key不仅仅比较hash值,还要比较这两个key的对象或值是否相等。这也是我们为什么说HashCode()和equals()要一起重写。
例如:
java
public int hashCode() {
return 1;//h 为一个随机数
}
public boolean equals(Object o) {
return true;
}
这种情况下,已经有一个[1,value]这样一个键值对,现在插入[2,value2]或任意键值对,这样就会直接认为这是同一个键值对,直接覆盖key为1的值。这与我们预期是不符的。
java
public int hashCode() {
return 1;//h 为一个随机数
}
public boolean equals(Object o) {
return true;
}
在这种情况下,已经存在一个[1,value]这样一个键值对,现在插入[1,value2],因为equals()返回false,会认为这两个键是不一样的,会再插入一个键值对。
put()方法
java
/**
* 实现Map.put和相关方法的核心方法
*
* @param hash 键的哈希值(通过hash()方法计算得到)
* @param key 要插入的键
* @param value 要插入的值
* @param onlyIfAbsent 如果为true,则不覆盖已存在的值
* @param evict 如果为false,表示表处于创建模式
* @return 返回旧值,如果没有则返回null
*/
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;
// 检查第一个节点是否匹配
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) {
p.next = newNode(hash, key, value, null);
// 链表长度达到树化阈值时转换为红黑树,树化阈值为8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 找到已存在的键
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果键已存在,处理值的更新
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 访问后回调(LinkedHashMap会使用)
afterNodeAccess(e);
return oldValue;
}
}
// 结构修改计数器,只有在插入之前没有存在该key时才会修改计数器,若已存在不会修改计数器
++modCount;
// 检查是否需要扩容
if (++size > threshold)
resize();
// 插入后回调(LinkedHashMap会使用)
afterNodeInsertion(evict);
return null;
}
// 树化查找方法
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null; // 用于记录key的比较类型
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; // 新节点应在右子树
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p; // 找到相同key的节点,直接返回
// 哈希值相同但key不相等,使用比较器或类的自然顺序
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);
}
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; // 插入成功,返回null
}
}
}
流程说明
-
哈希表初始化:如果哈希表为空或长度为 0,调用 resize () 方法进行初始化
-
计算索引 :通过
(n-1) & hash
计算键的存储位置 -
处理碰撞:
- 如果索引位置为空,直接创建新节点
- 如果不为空,检查第一个节点是否匹配
- 如果不匹配,检查是否为树节点
- 如果不是树节点,遍历链表查找或插入新节点
- 如果链表长度达到树化阈值(默认 8),将链表转换为红黑树
-
更新值:如果键已存在,根据 onlyIfAbsent 参数决定是否更新值
-
扩容检查:插入后检查元素数量是否超过阈值,超过则进行扩容
扩容方法resize()
java
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 1. 根据不同情况计算新容量和新阈值
if (oldCap > 0) {
// 情况1:旧容量已存在(非首次扩容)
if (oldCap >= MAXIMUM_CAPACITY) {
// 已达到最大容量,无法扩容,将阈值设为Integer.MAX_VALUE
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 正常扩容:容量翻倍(左移1位),阈值也翻倍
newThr = oldThr << 1;
}
else if (oldThr > 0)
// 情况2:旧容量为0,但阈值>0(使用带初始容量的构造函数时)
newCap = oldThr;
else {
// 情况3:使用无参构造函数,使用默认值
newCap = DEFAULT_INITIAL_CAPACITY; // 默认16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 16*0.75=12
}
// 2. 处理新阈值计算
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 3. 创建新哈希表数组
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 4. 迁移旧哈希表中的元素
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null; // 释放旧表的引用,帮助GC
// 情况1:桶中只有一个元素,直接计算新位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 情况2:桶中为树节点,调用树的split方法
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 情况3:桶中为链表,需要重新分配
else {
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 遍历链表,根据hash值将节点分为两组
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
// 低位链表:原位置
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
// 高位链表:原位置+oldCap
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;
}
容量增长机制:每次扩容为原容量的2倍,阈值也相应调整为原阈值的2倍
- 不同类型的元素迁移优化 :
- 单节点:直接计算新位置
- 树节点 :调用
split()
方法,可能会拆分红黑树或退化为链表 - 链表节点 :采用高低位分组策略,避免重新计算哈希值
(e.hash & oldCap) == 0
的节点留在原位置- 否则放在
原位置+oldCap
的新位置 - 这种方式确保迁移后链表元素顺序不变
- 性能优化点 :
- 使用 2 的幂作为容量,保证
(n-1) & hash
等同于取模运算 - 扩容时不需要重新计算每个元素的哈希值,只需判断新增的位是 0 还是 1
- 链表迁移采用尾插法,避免了 JDK 7 中头插法导致的死循环问题
- 使用 2 的幂作为容量,保证
树化方法
java
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 1. 检查哈希表容量是否小于最小树化容量(64)
// 如果容量不足,则优先扩容而不是树化
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 2. 容量足够时进行树化
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null; // 树节点的头指针和尾指针
// 3. 将链表节点转换为树节点,并保持原链表顺序
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);
// 4. 将新构建的树节点链表放入桶中,并调用treeify()方法转换为红黑树
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
- 容量检查机制 :
- 树化条件 :链表长度达到
TREEIFY_THRESHOLD
(默认 8) - 容量阈值 :哈希表总容量需达到
MIN_TREEIFY_CAPACITY
(默认 64) - 优先扩容:若容量不足,即使链表长度达标也会触发扩容而非树化 目的:通过扩容减少哈希冲突,可能使链表长度自然下降
- 树化条件 :链表长度达到
- 节点转换过程 :
- 将普通链表节点(
Node
)转换为树节点(TreeNode
) - 转换时保留原链表的顺序(通过
next
和prev
指针) - 树节点同时维护树结构(父节点、左右子节点)和链表结构
- 将普通链表节点(
- 红黑树构建 :
hd.treeify(tab)
方法将双向链表转换为红黑树- 红黑树的插入和平衡操作会调整节点顺序,但链表指针(
next
/prev
)保持不变 - 树化后,桶的第一个元素是红黑树的根节点,但仍可通过链表顺序遍历所有节点
- 为什么需要同时维护树和链表结构? :
- 链表顺序用于迭代器遍历(如
keySet().iterator()
) - 树结构用于快速查找(O (log n) 时间复杂度)
- 当元素被删除导致树节点过少时,可能会退化为链表(
untreeify()
方法)
- 链表顺序用于迭代器遍历(如
删除方法remove()
java
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;
// 1. 检查哈希表是否为空且对应桶是否有元素
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;
// 2. 检查第一个节点是否匹配
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 3. 第一个节点不匹配,遍历后续节点
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);
}
}
// 4. 找到目标节点后,检查是否需要值匹配
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 5. 根据节点类型执行删除操作
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;
// 6. 更新计数器并回调
++modCount;
--size;
afterNodeRemoval(node); // LinkedHashMap会使用
return node;
}
}
return null; // 未找到节点
}
- 节点定位 :
- 通过
(n-1) & hash
计算桶索引(利用 2 的幂特性优化取模) - 首先检查桶的第一个节点,多数情况下冲突较少,第一个节点即为目标
- 通过
- 链表与树的处理 :
- 链表 :遍历链表节点,通过
==
或equals()
比较键 - 红黑树 :调用
getTreeNode()
方法,利用树的结构快速查找(O (log n))
- 链表 :遍历链表节点,通过
- 删除操作 :
- 链表头节点:直接将桶指向原头节点的下一个节点
- 链表中间节点 :调整前驱节点的
next
指针跳过当前节点 - 树节点 :调用
removeTreeNode()
,可能触发树的平衡调整或退化为链表
- 红黑树的特殊处理 :
- 树节点删除后,可能需要通过旋转和颜色调整维持红黑树性质
- 如果删除后树节点过少(小于 6 个),会触发树退化为链表(
untreeify()
)
- 值匹配选项 :
matchValue
参数控制是否需要同时匹配键和值- 默认为
false
,即仅需键匹配即可删除