漫谈 HashMap
Intro
基本知识
- Map 接口实现类(Java 1.2+)
- 存储 K-V 键值对
- 无序,无下标,Key 唯一
- 线程不安全
- 允许一个 key 和多个 value 的取值为
null
相关知识
- 👉 哈希及 Java 实现
- 哈希函数
- 哈希冲突
- 👉 浅谈 equals() 和 hashCode()
- 作用
- 默认实现
- 重写
1、底层结构
1.1、版本区别
Java 1.8 对 HashMap 的底层结构做了改动,
在原先的基础上引入了红黑树。
-
Java 1.7-:数组 + 位桶(链地址法)
-
Java 1.8+:数组 + 位桶,红黑树
- 链表查询元素的时间复杂度是
O(n)
,若哈希冲突频繁会导致链表过长,效率低。 - 红黑树查询元素的时间复杂度是
O(logn)
。
- 链表查询元素的时间复杂度是
1.2、红黑树
1.2.1、树化 & 退化
树化条件:需同时满足两个条件
- 位桶结点数 >= 树化阈值(默认 8)
- 哈希表结点数 >= 最小树化容量(默认 64,根据泊松分布得出)
退化条件:位桶结点数 <= 退化阈值(默认 6)
删除结点、扩容(易忽略)时可能触发退化。
退化阈值肯定不超过树化阈值,那么阈值的大小有什么影响?
- 过大:与树化阈值接近,反复的树化和退化影响性能。
- 过小:结点数少的时候,红黑树性能反而不高,应当尽早退化。
1.2.2、为什么是红黑树
为何采用红黑树,而不是其它树。
-
二叉排序树(BST):可能产生线性结构。
- 在极端情况下,添加的元素都比根节点大或者小,导致一侧子树线性增长。
- 此时的结构无异于链表,时间复杂度是
O(n)
。
-
自平衡二叉查找树(AVL):与红黑树都是平衡二叉搜索树,区别在于增删结点时的旋转操作。
AVL 树 红黑树 说明 严格平衡,平均查询效率更快,但需要更多旋转次数 需要最多 2 次旋转 查找时间复杂度 O(logn)
O(logn)
旋转时间复杂度 O(logn)
O(1)
适用场景 查找密集型任务 增删密集型任务
2、成员结构
2.1、位桶 (bucket)
位桶(bucket):哈希表的基本存储单元。
- Java 1.7 之前称为
Entry<K, V>
,采用头插法。 - Java 1.8 之后称为
Node<K, V>
,采用尾插法。
2.1.1、源码
HashMap 的静态内部类
成员变量:
-
哈希值
-
结点的键(关键码)
-
结点的值
-
后继结点
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; // 构造方法 // getKey()、getValue() // toString()、hashCode()、equals() // setValue() }
2.1.2、TODO
- HashMap 1.7 并发扩容
- 红黑树结点
2.2、成员变量
HashMap 的所有成员变量均为
default
,外部只能通过特定接口进行操作(封装)。
概念区分
容量 vs 大小
Hint:结点数量 = 键值对数量
- 容量(capacity):哈希表的数组长度(最多可容纳的结点数量)。
- 默认容量
1<<4
(16
),根据泊松分布得出。 - 为了保证 HashMap 的性能,容量始终是 2 的幂。
- 默认容量
- 大小(size):实际存储的结点数量。
- 哈希表大小:哈希表中的结点数量。
- 位桶大小:链表(或红黑树)中的结点数量。
扩容 vs 树化
- 含义:
- 扩容:扩充哈希表的数组长度,从而能存储更多链表。
- 树化:针对现有的链表,在符合条件的情况下转换为红黑树。
- 当哈希表存储的结点数增加时,如何从扩容、树化中抉择呢?
- Java 为了避免二者的冲突,要求最小树化容量 > 4 * 树化阈值。
- 即优先选择扩容,使链表长度降低。
2.2.1、重要参数(❗)
含义 | 说明 | 默认值 | |
---|---|---|---|
table | 哈希表 | Node<K, V> 类型的数组 | - |
DEFAULT_INITIAL_CAPACITY | 默认初始容量 | 使用无参构造方法时,数组默认的长度 | 1<<4 (i.e. 16 ) |
size | - | 哈希表结点数 | - |
threshold | 扩容阈值 | size > 扩容阈值时,哈希表会扩容并重新映射 (扩容阈值 = 哈希表容量 * 加载因子) |
12 |
loadFactor | 加载因子 | 哈希表扩容之前允许的最大存储度量,需要在时间和空间成本之间折衷(过大导致链表太长,过小导致频繁扩容) | 0.75f |
TREEIFY_THRESHOLD | 树化阈值 | 树化的条件之一:位桶结点数 >= 树化阈值 (参考 putVal() 源码) |
8 |
UNTREEIFY_THRESHOLD | 退化阈值 | 退化条件:位桶结点数 < 退化阈值 | 6 |
MIN_TREEIFY_CAPACITY | 最小树化容量 | 树化的条件之一:数组长度 >= 最小树化容量 (参考 treeifyBin() 源码) |
64 |
2.2.2、其它参数
含义 | 说明 | |
---|---|---|
MAXIMUM_CAPACITY | 最大容量 | 1<<30 |
modCount | 修改数 | 记录哈希表的修改次数,用于多线程下的 fail-fast 机制 |
entrySet | 包含所有 K-V 的 Set 集合 | |
keySet | 包含所有 K 的 Set 集合 | 继承自 AbstractMap 类 |
values | 包含所有 V 的 Collection 集合 | 继承自 AbstractMap 类 |
2.3、构造方法
HashMap
有 4 个构造方法,可指定不同参数。哈希表(table)在构造方法中尚未初始化,首次添加元素的时候才初始化。
① 初始容量、加载因子
指定期望容量、加载因子
-
初始容量:必须大于 0,小于最大容量(2 ^ 30)。
- 此处的 initialCapacity 仅代表期望容量。
- 不一定会用作哈希表数组的初始长度。
-
加载因子:必须是大于 0 的合法浮点数。
-
扩容阈值:tableSizeFor()
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); }
② 初始容量
指定初始容量:
调用首个构造方法,默认加载因子
0.75f
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
③ 无参
默认加载因子
0.75f
。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
④ Map
传入一个 Map 集合
-
指定默认加载因子
0.75f
。 -
调用
putMapEntries(...)
,批量添加元素(深拷贝)。public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
putMapEntries(...)
提供给 HashMap 的构造方法和 putAll(...) 使用。
参数:
- m:Map 集合
- evict:false 表示初始化时调用。
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
// 当前哈希表为空 -> 初始化扩容阈值
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
// 当前哈希表非空 -> 扩容
else if (s > threshold) // 此构造方法没有赋扩容阈值,此时 threshold == 0
resize();
// 深拷贝
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
3、确定存储位置
3.1、hash() 扰动函数
Hint:对象的
hashcode
不会直接用于哈希表。
- 分析:
- 哈希表容量(数组长度)是有限的。
- 对象的
hashCode()
通常超出了哈希表长度范围。
- 对策:扰动函数
hash()
HashMap
定义的静态方法。- 用于进一步处理
hashCode()
,得到合适的哈希值。
Java 1.7
(扰动函数)4 次位运算 + 5 次异或运算
作用:防止低位不变、高位变化时,造成的哈希冲突。
static final int hash(Object k) {
int h = 0;
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
Java 1.8
(优化的扰动函数)1 次位运算 + 1 次异或运算
- 所有 bit 都能参与运算。
- 减少处理次数,提高性能。
做法:高 16 位与低 16 位进行异或运算。
-
将对象的 hashCode 右移 16 位,丢弃低 16 位(此时高 16 位全为 0)
-
将位运算结果(高 16 位)与对象 hashCode 做异或运算。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
3.2、indexfor() 确定下标
indexfor():计算元素在哈希表中的存储位置(数组下标)。
Java 1.7 单独定义了该方法,
Java 1.8+ 将代码逻辑移到
put()
中。
static int indexFor(int h, int length) {
// 二进制与运算
return h & (length-1); // 等价于 h % length(十进制取余运算)
}
Hint:对于计算机,二进制运算速度比十进制快。
分析:为什么
h & (length-1)
等价于h % length
。
-
哈希表容量(
length
)始终是 2 的幂(二进制的低位有多个 0)。 -
h 对 length 的取余结果 <= length - 1,即无法整除的部分
-
h 和 length - 1 的与运算,代表 h 低位无法整除 length 的部分,即余数。
4、添加元素 put()
Hint
- 哈希表(table)在构造方法中尚未初始化,首次添加元素的时候才初始化。
- HashMap 没有专门提供修改元素的方法,可以通过
put()
覆盖旧值。
思维导图
源码共分为四个阶段。
Hint:若方法返回值非 null,说明哈希表中已存在相同 key 的结点。
-
初始扩容:判断哈希表为空或长度为 0,进行扩容。
-
存储位置:计算元素的存储位置(数组下标)。
- 位置为空:直接赋值,进入第四阶段。
- 位置非空:发生了哈希冲突,进入第三阶段。
-
拉链法:
- 判断首个结点与待插入元素的 hash 和 key 是否相等,是则跳到 4。
- 判断首个结点是否是红黑树,是则调用 putTreeVal() 添加红黑树结点(之后进入第四阶段)。
- 双指针遍历链表
- 判断是否到表尾,是则尾插入、判断树化条件(之后进入第四阶段)。
- 判断当前结点与待插入元素的的 hash 和 key 是否相等,是则跳到 4。
- 存在结点与待插入元素的 hash 和 key 相等,覆盖 value 值并返回旧值(方法结束)。
-
扩容:判断哈希表结点数超过扩容阈值,进行扩容。
put()
调用
hash()
扰动哈希值,调用
putVal()
添加元素。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
putVal()
/*
hash - 扰动后的哈希值,用于计算存储位置(数组下标)
key - 元素的键
value - 元素的值
onlyIfAbsent - 当哈希表中对应下标位置为空才添加
evict - false 表示初始化时调用。
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// -----------------------(一)初始扩容-----------------------
/*
tab:当前哈希表
p:数组下标为 i 的元素
n:哈希表结点数
i:元素存储位置(数组下标),即 indexfor() 计算结果
*/
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 判断哈希表为空或长度为0 -> 扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// -----------------------(二)存储位置-----------------------
// 计算存储位置,即 indexfor()
// 判断存储位置为空 -> 直接赋值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// -----------------------(三)拉链法-----------------------
else { // 位置不为空(哈希冲突)
/*
e:若非空,表示当前链表(或红黑树中)与待添加元素的key相同的结点
k:指针e的key
*/
Node<K,V> e; K k;
// 链表首个结点与待插入元素的hash和key相同 -> 确定e
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 { // 双指针遍历链表(p和e)
/*
binCount:遍历次数,最多不超过链表长度
p:最初指向数组下标i
e:表示当前指向的结点,最初指向 p.next
*/
for (int binCount = 0; ; ++binCount) {
// 遍历到表尾 -> 插入新结点(Java 1.8 尾插法)
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// binCount + 1 表示添加新结点后的链表长度(包括数组下标 i 的元素)
// 判断当前链表长度超过树化阈值 -> 树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 当前结点的hash和key与待插入结点相同 -> 确定 e,结束遍历
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 更新指针
p = e;
}
}
// 说明链表中存在相同的key -> 覆盖value值
if (e != null) { // existing mapping for key
/*
e.value:旧value值
value:新value值
*/
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); // 在HashMap中是空实现
// 返回旧值(方法结束)
return oldValue;
}
}
// 修改数+1
++modCount;
// -----------------------(四)扩容-----------------------
// 判断哈希表结点数超过扩容阈值 -> 扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict); // 在HashMap中是空实现
return null;
}
HashMap
中的模板方法模式在 HashMap 中定义并提供空实现,LinkedHashMap 子类重写了具体逻辑。
-
afterNodeAccess:将被访问到的结点移动到链表尾部(LRU 算法)
-
afterNodeInsertion:链表长度超过容量时,移除链表头个元素(LRU 算法)
-
afterNodeRemoval:用于移除双向链表中的结点。
// Callbacks to allow LinkedHashMap post-actions void afterNodeAccess(Node<K,V> p) { } void afterNodeInsertion(boolean evict) { } void afterNodeRemoval(Node<K,V> p) { }
putIfAbsent()
(Java 1.8+)底层调用
putVal()
方法,指定
onlyIfAbsent = true
@Override public V putIfAbsent(K key, V value) { return putVal(hash(key), key, value, true, true); }
5、扩容 resize()
扩容:哈希表的数组长度和扩容阈值变为原来的 2 倍,并再次散列。
- 版本区别:
- Java 1.7-:扩容后,链表结点顺序倒置。(HashMap 1.7 并发扩容)
- Java 1.8+:会保持原有顺序。
- 触发场景:
- 初始化:哈希表为空或长度为 0,进行初始化。
- 扩容:哈希表的结点数超过扩容阈值,扩容 2 倍。
思路
源码主要分为两个阶段。
注意:扩容指的是数组长度,
- 确定新容量和扩容阈值:
- 判断数组长度是否已达上限,是则不再扩容(方法结束)。
- 判断数组已初始化:确定新容量和扩容阈值为原来的 2 倍(跳到 5)。
- 数组未初始化:判断扩容阈值是否已赋值,从而决定初始化时的容量和扩容阈值。
- 大于 0:说明调用的是有参构造方法(指定了期望容量和加载因子),将当前扩容阈值(oldThr)作为初始容量(跳到 4)。
- 小于等于 0:说明调用的是空参构造方法,使用默认初始容量 16 和默认扩容阈值(跳到 5)。
- 针对调用有参构造方法的情况,基于实际初始容量计算扩容阈值(newThr)。
- 更新扩容阈值。
- 创建新数组,重新散列:
- 若原哈希表数组尚未初始化,则直接返回初始化完成的新数组(方法结束)。
- 遍历原数组的每个位置(空/单个结点/链表/红黑树)
- 用指针保存当前遍历到的数组位置,将其空间释放。
- 若是单个结点,则直接计算新存储位置并保存到新数组。
- 若是红黑树,则分割树节点并插入新数组。
- 若是链表,分成两段(称为 low 区、high 区),分别插入到新数组的下标 i 和偏移量 oldCap + i。
resize()
final Node<K,V>[] resize() {
/*
oldTab:当前哈希表
oldCap:当前数组长度
oldThr:当前扩容阈值
newCap:新数组长度
newThr:新扩容阈值
*/
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// -----------------------(一)确定新容量和扩容阈值-----------------------
// ----------数组已初始化-----------
if (oldCap > 0) {
// 数组长度已达最大容量 -> 不再扩容,返回当前哈希表
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 扩容:新容量2倍,扩容阈值2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// ----------数组未初始化-----------
// 扩容阈值大于0,说明调用的是有参构造方法,指定过初始容量和加载因子
else if (oldThr > 0) // initial capacity was placed in threshold
// 将扩容阈值作为数组初始容量(此时不是使用构造方法中的期望容量)
newCap = oldThr;
// 空参构造方法进入else块
else { // zero initial threshold signifies using defaults
// 初始容量16,扩容阈值12
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 数组未初始化,且扩容阈值大于0(即上面的else if块的情况)
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;
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)
newTab[e.hash & (newCap - 1)] = e;
// 已形成红黑树 -> 分割树结点并插入新数组
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 普通链表
else { // preserve order
/*
扩容后的数组长度是原来 2 倍,而结点存储位置 = hash % 数组长度
因此,针对原哈希表的所有链表(或红黑树)上的结点,新存储位置有两种散列情况:
1. 相同位置:称为低位区(low)
2. 扩充位置:称为高位区(high)
loHead和loTail分别代表low区的头尾指针,lhiHead和hiTail分别代表high区的头尾指针,
*/
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
// 指向原链表当前遍历的结点
Node<K,V> next;
do {
next = e.next;
// oldCap 二进制始终是【... 1000】
if ((e.hash & oldCap) == 0) { // 说明 hash取余结果 < oldCap,放在低位
// 判断 low 区没有元素 - > 直接插入
if (loTail == null)
loHead = e;
// 判断 low 区已形成链表 -> 尾插到loTail
else
loTail.next = e;
// 更新low区尾指针
loTail = e;
}
else { // 放在高位,逻辑同上
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 将low区放到新数组的相同位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 将high区放到新数组的扩充位置
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
关于 low 和 high
扩容后的数组长度是原来 2 倍,相当于多出一段与原来相等长度的存储空间。
针对旧哈希表的同一条链表,会被拆分成两条链表,分别移到新哈希表的两段等长空间。
- Java 1.7-:重新计算每个结点在新哈希表中的位置(
i = hash & (newCap - 1)
) - Java 1.8+:判断每个结点是否满足
(hash & oldCap) == 0
,从而对应两段等长空间。
以下分析 Java 1.8 算法的原理。
示例
以 oldCap = 16(0010 0000)为例,
其不同二进制值如下。
n | n - 1 | |
---|---|---|
old | 0010 0000(旧容量) | 0001 1111(旧容量 - 1) |
new | 0100 0000(新容量) | 0011 1111(新容量 - 1) |
以实际哈希值 h 为例,观察二进制与运算的结果。
-
示例一:225(1001 0101)
1001 0101(示例 1) 与运算结果 说明 0001 1111(oldCap - 1) 0001 0101 旧下标 0011 1111(newCap - 1) 0001 0101 新下标 = 旧下标 0010 0000(oldCap) 0000 0000 低位是 0,最高位和 newCap - 1 最高位相同 -
示例二:173(1001 0101)
1010 1101(示例 2) 与运算结果 说明 0001 1111(oldCap - 1) 0000 1101 旧下标 0011 1111(newCap - 1) 0010 1101 新下标 = 旧下标 + 偏移量 0010 0000(oldCap) 0010 0000 低位是 0,最高位和 newCap - 1 结果最高位相同
分析
分析结果
- h & oldCap - 1:旧下标。
- h & newCap - 1:新下标 = 旧下标 (+ 偏移量)
- 是否添加偏移量,取决于 newCap - 1 的最高位取值。
- 低位信息代表偏移量,可以由后续统一添加。
- h & oldCap:最高位是 0 或 1,低位始终是 0。
结论:新下标的结果取决于 newCap - 1 最高位(恰好也是 oldCap 最高位)。
- Java 1.7-:每个结点都计算偏移量。
- Java 1.8+:先判断是否需要偏移,再为偏移部分统一添加偏移量。
6、查询 get()
根据 key 获取 value,
若 key 不存在则返回 null。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
getNode()
-
计算存储位置(数组下标),即
indexfor()
算法逻辑。 -
判断下标为
indexfor()
的元素是否为待查找元素,是则返回。 -
判断是红黑树结点,调用
getTreeNode()
获取并返回红黑树结点。 -
双指针遍历链表,直到找到待查询元素(若无,则返回 null)。
final Node<K,V> getNode(int hash, Object key) { /* tab:当前哈希表 first:根据hash计算的数组下标位置的元素(链表的首个结点) e:双指针算法 n:哈希表结点数 i:元素存储位置(数组下标),即 indexfor() 计算结果 */ 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) { // 判断是红黑树结点 -> 调用getTreeNode()获取 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; }
getOrDefault()
(Java 1.8+)调用
getNode()
并指定默认值,如果找不到目标 key 则返回默认值。
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}
7、删除 remove()
HashMap 有两个
remove()
,分别来自重写、重载。
-
重写:删除 key 对应的结点,且要求 key 的 value 值与指定 value 值相等才删除。
-
重载:删除 key 对应的结点。
@Override public boolean remove(Object key, Object value) { return removeNode(hash(key), key, value, true, true) != null; } public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; }
参数
- matchValue:value 值与指定 value 值相等才删除。
- movable:移除红黑树结点时允许移动其它结点。
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
/*
tab:当前哈希表
p:下标为index的数组元素
e:双指针算法
n:哈希表结点数
index:元素存储位置(数组下标),即 indexfor() 计算结果
*/
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:存储待删除结点
e:双指针算法
k/v:当前指向结点的键/值
*/
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) {
// 判断红黑树结点 -> 遍历红黑树找到node
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);
}
}
// 找到待删除结点、满足删除value的条件
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;
// 删除表中结点(此时p指向node的前一个结点)
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
8、迭代器
- HashIterator:
- KeyIterator
- ValueIterator
- EntryIterator
- KeySet、Values、EntrySet 的 Iterator(Collection 接口定义)
9、对比 Hashtable
Hashtable | HashMap | |
---|---|---|
线程安全 | ✔ | ❌ |
K/V 为 null | 不允许 | 允许一个 key、多个 value |
容量 | 默认 16,始终为 2 的幂 | 默认 11 |
哈希值 | 对象 hashCode() | 对象 hashCode() + 扰动函数 hash() |
存储位置 | 取余 % (十进制) |
与 & (二进制) |
扩容 | 2 倍 + 1 | 2 倍 |