HashMap 继承关系
HashMap 是 JDK 1.2 提供的类,它继承自 AbstractMap,并实现了 Map、Cloneable、Serializable 接口。说明 HashMap 具有 Map 的全部实现,且 HashMap 默认支持浅拷贝,序列化和反序列化。具体的实现可以查看 HashMap 的 clone()、writeObject()、readObject() 方法。
通过 HashMap 的文档可以得到以下信息:
- HashMap 是以 key-value 键值对的形式保存数据的,但是 HashMap 无法保证 key-value 键值对的迭代顺序为插入顺序。
- HashMap 只允许一个 null 键和允许多个 null 值。
- HashMap 内部是使用 Hash 桶的形式存储数据的,HashMap 可以保证插入的键值对均匀的分布在不同的 Hash 桶内。
- 影响 HashMap 性能的指标有两个: 初始容量和负载因子,容量就是 Hash 桶的数量,初始容量就是创建 HashMap 时的 Hash 桶的数量,容量并不是指 HashMap 中元素的数量,这点要区分开。每当 HashMap 当前元素的数量 = 当前容量 * 负载因子则会自动扩容,扩容后的容量为当前容量的 2 倍。所以为了避免频繁扩容一定要在初始化 HashMap 时指定合理的初始容量。
- HashMap 是非线程安全的,如果在迭代器迭代时存在线程对其进行结构修改则抛出 ConcurrentModificationException 异常,这是迭代器的 fail-fast 策略。
HashMap 数据结构总览
可以看到 HashMap 底层使用了数组,也就是哈希桶存储键值对,每个键值对在数组的位置是根据 hash 函数计算得到,因为数组长度有限,很可能发生哈希冲突,这就引进了链表和红黑树,当链表容量到了一定长度则转换为红黑树以提升查询、插入效率,可见 JDK 1.8 HashMap 的作者将数据结构玩的出神入化,所以 HashMap 才有了现在的出色性能。
HashMap API 实现
接下来看一下 HashMap API 具体的实现细节,来了解 HashMap 是如何工作的。
静态常量
// 默认初始容量,桶的个数 16 2 的 4 次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量 2 的 30 次方
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 树化阈值 一个桶链表长度超过 8 进行树化
static final int TREEIFY_THRESHOLD = 8;
// 链表化阈值 一个桶中红黑树元素少于 6 则从红黑树变成链表
static final int UNTREEIFY_THRESHOLD = 6;
// 最小树化容量,当容量未达到 64,即使链表长度 >8,也不会树化,而是进行扩容
static final int MIN_TREEIFY_CAPACITY = 64;
成员变量
// Hash 桶数组,存储数据的底层结构
transient Node<K,V>[] table;
// 保存缓存的 entrySet,注意 AbstractMap 中的是 keySet 和 values
transient Set<Map.Entry<K,V>> entrySet;
// HashMap 中键值对的数量,也就是元素的数量,注意和容量区分开,容量指的是 table[] 的数量
transient int size;
// HashMap 结构修改的次数,即插入和删除操作,迭代器会使用
transient int modCount;
// HashMap 扩容 table[] 的阈值 threshold = capacity * loadFactor,当HashMap 中元素的数量达到此阈值需要考虑扩容
int threshold;
// 负载因子
final float loadFactor;
数据结构
// 1
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;
}
}
// 2
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);
}
}
- Node 是 HashMap 的静态内部类,实现了 Map.Entry 接口,是 HashMap 用来存储 key-value 的数据结构,代表一个链表节点,每个 Node 节点是一个 key-value 映射,且每个节点有一个指向其下个节点的指针,说明这是一个单链表。而且 Node 的 hash 成员变量和 key 成员变量都是 final 修饰的说明不可改变,HashMap 也确实没有提供修改 key 的 API。
- TreeNode 是 HashMap 的静态内部类,继承了 LinkedHashMap.Entry,代表一个红黑树节点。
现在再看 HashMap 更直观一点的底层数据结构大概是这样(图片原文地址:HashMap Jdk8的实现原理)
构造方法
public HashMap() {
// 1
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
public HashMap(int initialCapacity) {
// 2
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
// 3
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
// 4
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 5
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
// 6
this.loadFactor = loadFactor;
// 7
this.threshold = tableSizeFor(initialCapacity);
}
/**
* Returns a power of two size for the given target capacity.
* 返回指定容量的 2 的幂,返回的值 >= cap
*/
// 8
static final int tableSizeFor(int cap) {
// 为了防止 cap 就是 2 的幂次,如果不 -1,经过下面的算法会把最高位后面的都置位 1,再加 1 则相当于将当前的数值乘 2
// 比如 cap = 8 如果不 -1 则最终输出 16 并不是 8
int n = cap - 1;
// 高位右移 1 位,保障高位和第二高位都为 11
n |= n >>> 1;
// 高两位右移 2 位,保障高 4 位都为 1
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
// 保障最高的可达 32 位都为1
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
- 无参构造器仅仅是设置 HashMap 的 loadFactor 成员变量为默认的 loadFactor,且 loadFactor 是 final 修饰的说明不可修改。使用无参构造器 threshold 是默认的 0。
- 使用自定义的初始容量和默认的负载因子创建 HashMap 实例。
- 指定初始容量和负载因子创建 HashMap 实例,因为不建议修改 loadFactor,所以此构造方法不经常显示使用。
- 对初始容量和负载因子进行有效性验证。
- 如果初始容量超过了 HashMap 允许的最大值则使用最大值作为初始容量,最大容量是 2 的 30 次方。
- 给 loadFactor 成员变量赋值。
- 将 >= 指定初始容量的最小 2 的 n 次方赋值给 threshold 成员变量,即如果你指定的初始容量是 15,这里则是 16,因为 HashMap 要求Hash 桶的容量必须是 2 的幂。当下一个元素插入时 HashMap 键值对的数量(并不是 Hash 桶的数量)size >= threshold 时要考虑扩容,threshold 是衡量 Hash 桶是否需要扩容的一个标准。此外如果数组还没有分配空间,threshold 保持初始容量,或为零,表示默认初始容量。
- 流程图如下
- 总结一下,以上构造器并没有初始化 table[],只是对 loadFactor 和 threshold 进行初始化。
HashMap 还有一个构造方法接受一个 Map 类型的入参,这也是 Map 接口要求的。
// 新建一个 HashMap 并将指定的 m 的键值对添加到其中
public HashMap(Map<? extends K, ? extends V> m) {
// 使用默认的负载因子
this.loadFactor = DEFAULT_LOAD_FACTOR;
// 将指定 m 的所有元素加入表中,参数 evict 初始化时调用为false,其他情况为 true
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
// 如果指定的 m 不是空的才进行操作
if (s > 0) {
.// 如果 table 还没有进行初始化
if (table == null) { // pre-size
// 根据 m 的元素数量和当前 HashMap 的加载因子计算阈值
float ft = ((float)s / loadFactor) + 1.0F;
// 修正阈值的边界 不能超过 MAXIMUM_CAPACITY
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
// 如果新的阈值大于当前阈值
if (t > threshold)
// 返回一个 >= 新的阈值的满足 2 的 n 次方的阈值
threshold = tableSizeFor(t);
}
// 如果当前元素表不是空的,但是 m 的元素数量大于阈值,说明要扩容
else if (s > threshold)
// 插入数据前先扩容
resize();
// 遍历 m 依次将元素加入当前表中。
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
// 说明是浅拷贝,当前 HashMap 和 m 使用的是 key 和 value 的相同引用
putVal(hash(key), key, value, false, evict);
}
}
}
put(K key, V value)
public V put(K key, V value) {
// 1
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
// 2
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// onlyIfAbsent 为 true 表示不会覆盖 key 的 value 值,evict 为 fasle 表示初始时调用
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// tab 存放当前的哈希桶 p 用作临时链表节点
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 3
if ((tab = table) == null || (n = tab.length) == 0)
// 4 此处 resize() 是初始化
n = (tab = resize()).length;
// 如果当前 index 的节点是空的,表示没有发生哈希碰撞 直接构建一个新节点 Node,挂载在 index = (n - 1) & hash 处
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 发生了哈希冲突
else {
Node<K,V> e; K k;
// p 是 链表的头结点或者红黑树的根节点
// 如果p 的哈希值和 key 的哈希值相等,key 也相等则是覆盖 value 操作
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 当前节点指向 p
e = p;
// 如果 p 节点是红黑树
else if (p instanceof TreeNode)
// 将新节点插入到红黑树,如果 e 不是 null 则表示替换红黑树的节点
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 如果当前节点是链表
else {
// 遍历链表
for (int binCount = 0; ; ++binCount) {
// e 指向 p.next
// 如果 e 是 null 即遍历到尾部,追加新节点到尾部
if ((e = p.next) == null) {
// 新节点放到 p 的下一个节点
p.next = newNode(hash, key, value, null);
// 如果此时链表数量不算新追加的节点 - 1 >=8 则链表转化为红黑树
// 插入第 9 个节点时会树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 6 树化
treeifyBin(tab, hash);
break;
}
// 如果在链表找到了要覆盖的节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 跳出
break;
// 当前节点指向 e ,即 p = p.next
p = e;
}
}
// 如果 e 不是 null,说明 e 是需要被覆盖的节点
if (e != null) { // existing mapping for key
// 取出旧值原来返回
V oldValue = e.value;
// 如果 onlyIfAbsent 为 false 或者 oldValue 是 null 则覆盖
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 空方法 用作 LinkedHashMap 重写使用
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 结构修改次数 + 1
++modCount;
// size + 1,并判断当前 size 是否达到阈值需要扩容。
if (++size > threshold)
// 此处 resize() 是扩容
resize();
// 空实现 用作 LinkedHashMap 重写使用
afterNodeInsertion(evict);
// 返回 null
return null;
}
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
- 将指定的 key value 映射。如果 key 存在一个 oldValue 值则返回 oldValue 值,返回的值是 null 可能是因为 oldValue 是 null 或者 key 不存在。
- 计算 key 的 hash,如果 key 是 null 值则 hash 为 0,否则使用 (h = key.hashCode()) ^ (h >>> 16) 计算 key 的 hash 值。为什么不直接使用 key.hashCode() 作为 hash 反而要再进行一次位运算?原因是为了元素能均匀的分布在 hash 桶上,减小 hash 碰撞的概率。hash 桶是一个 Node[],将新的 Node 存储在数组上必须要知道它在数组的下标,HashMap 是使用 (n - 1) & hash 的方式计算出下标的, 因为 key.hashCode() 的返回值是一个 int 型,int 型取值范围很大但是 HashMap 中桶的数量很小,如果直接使用 key.hashCode() 只是使用了其低 16 位,高 16 位往往没有使用,这增加了 hash 碰撞的几率,通过右移 16 位操作后就可以使 key.hashCode() 的高 16 位参与到下标计算中,减少了 hash 碰撞的概率,举个例子,假设当前桶的数量是 16,新插入一个key,那么计算下标的过程就如下图所示:
- 如果当前 table[] 是空的需要进行初始化,如果使用了 HashMap()、HashMap(int)、HashMap(int,int) 构造方法创建实例,此时 table[] 数组都是空的,说明 HashMap 的 table[] 初始化是在第一次 put 操作时。
- 如果 table[] 数组是 null,即还没有初始化或者长度是 0,需要对数组的容量进行调整,如果 tabke[] 为空,则根据默认初始容量进行分配。否则加倍 table[] 的容量,因为 table[] 的长度每次都是 2 的幂,所以每个元素必须保持在相同的索引,或在扩容后的 table[] 中移动 2 的幂次方的偏移量。
// 对哈希桶进行初始化或者扩容
final Node<K,V>[] resize() {
// oldTab 为当前哈希桶
Node<K,V>[] oldTab = table;
// 当前哈希桶的容量,即 table[] 数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 当前的阈值
int oldThr = threshold;
// 新的容量和阈值
int newCap, newThr = 0;
// 如果当前容量大于0
if (oldCap > 0) {
// 如果当前容量已达到上限
if (oldCap >= MAXIMUM_CAPACITY) {
// 设置扩容阈值是 2 的 31次方 - 1
threshold = Integer.MAX_VALUE;
// 返回当前哈希桶不在扩容
return oldTab;
}
// 如果新的容量是旧的两倍且旧的容量大于等于 16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 新的阈值也是旧的阈值的 2 倍
newThr = oldThr << 1; // double threshold
}
// 如果当前哈希桶是空的但是阈值不为0,代表创建实例时指定了初始容量
else if (oldThr > 0) // initial capacity was placed in threshold
// 设置新的容量 = 当前阈值,阈值此时就是初始容量
newCap = oldThr;
else { // 如果当前哈希桶是空的,而且当前阈值也是 0 代表是创建实例时没有指定初始容量/阈值
// 容量设置为默认的容量 16
newCap = DEFAULT_INITIAL_CAPACITY;
// 新的阈值为默认容量 16 * 默认加载因子 0.75f = 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新的阈值是 0,说明走的是 else if (oldThr > 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;
// 下面开始将当前哈希桶中的所有节点转移到新的哈希桶中
if (oldTab != null) {
// 遍历旧的哈希桶
for (int j = 0; j < oldCap; ++j) {
// 代表当前遍历的节点
Node<K,V> e;
// 如果当前桶中有元素
if ((e = oldTab[j]) != null) {
// 将桶置空以便 GC
oldTab[j] = null;
// 如果当前链表只有一个元素,没有发生哈希碰撞
if (e.next == null)
// 通过 e.hash & (newCap - 1) 计算出此元素在新的哈希桶中的位置
// 因为 newCap 永远是 2 的幂次,所以这就相当于是对数组容量进行取模运算的优化写法
newTab[e.hash & (newCap - 1)] = e;
// 如果发生过哈希碰撞 ,而且是节点数超过8个,转化成了红黑树
else if (e instanceof TreeNode)
// 5
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。
else { // preserve order
// 因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即 low 位
// 或者扩容后的下标,即 high 位, high 位= low 位 + 原哈希桶容量
// 低位链表的头结点、尾节点
Node<K,V> loHead = null, loTail = null;
// 高位链表的头节点、尾节点
Node<K,V> hiHead = null, hiTail = null;
// 临时节点 存放 e 的下一个节点
Node<K,V> next;
do {
next = e.next;
// 这里又是一个利用位运算代替常规运算的优化
// 利用哈希值与旧的容量通过与运算的值判断大于等于 oldCap 还是小于 oldCap
// 等于 0 代表小于 oldCap,应该存放在低位,否则存放在高位
if ((e.hash & oldCap) == 0) {
// 如果低位尾节点是 null
if (loTail == null)
// e 当作低位头节点
loHead = e;
// 如果低位尾节点不是 null
else
// 低位尾节点的下一个节点指向 e
loTail.next = e;
// 低位尾节点指向 e
loTail = e;
}
// 存放在高位
else {
// 如果高位尾节点是 null
if (hiTail == null)
// e 当作高位头节点
hiHead = e;
// 如果高位尾节点不是 null
else
// 高位尾节点的下一个节点指向 e
hiTail.next = e;
// 高位尾节点指向 e
hiTail = e;
}
// 如果链表节点的下一个节点是 null 则已经遍历完此链表
} while ((e = next) != null);
// 如果低位尾节点不是 null
if (loTail != null) {
// 将低位尾节点的 next 节点指向 null
loTail.next = null;
// 将低位头节点插入到原 index
newTab[j] = loHead;
}
// 如果高位尾节点不是 null
if (hiTail != null) {
// 将高位尾节点的 next 节点指向 null
hiTail.next = null;
// 将高位头节点插入到新 index = 原 index + 原容量
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 返回新的哈希桶
return newTab;
}
如图:是一个扩容的前后对比,扩容后,原 index 的元素在新的哈希桶中存在两个位置的可能,一个就是原 index,即低位,另一个就是原 index + 原数组容量计算得到的 index,即高位。
- 修剪红黑树,可能会把一个大的红黑树修建为两个小红黑树,一个依旧保留在原 index(低位),一个移动到原 index+原容量(高位)。然后分别判断两个红黑树是否需要去树化。和链表的操作相似。
// map 当前 HashMap 实例
// tab 扩容后的新数组
// 当前元素需要插入哈希桶的索引位
// 旧的哈希桶容量
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;
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
// 区分高低位,和链表的类似 如果 e.hash & bit = 0 表示依旧存在低位
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
// 低位节点数量 +1
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
// 高位节点数量 +1
++hc;
}
}
if (loHead != null) {
// 如果低位节点数量 <= 6
if (lc <= UNTREEIFY_THRESHOLD)
// 对低位进行去树化 TreeNode 转换为 Node
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
// 如果低位区小红黑树元素个数大于 6 且高位区红黑树不为 null 时,开始对低位树化操作(赋予红黑树的特性)
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
// 高位 = 低位(原 index) + 原容量
if (hiHead != null) {
// 如果高位节点数量 <= 6
if (hc <= UNTREEIFY_THRESHOLD)
// 对高位进行去树化 TreeNode 转换为 Node
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
// 如果高位区小红黑树元素个数大于 6 且低位区红黑树不为 null 时,开始对高位树化操作(赋予红黑树的特性)
hiHead.treeify(tab);
}
}
}
- 对链表尝试转换为红黑树。如果符合转换的条件则将所有的节点转换成树形节点,并且构造成双链表为 treeify 转换成红黑树准备。
treeify 方法的主要作用是将链表的元素一个一个的插入到树中,并且保持排序树的特性当左、右子树不为空的时候,左子树小于根节点,右子树大于根节点。这里的大小通过 comparable 方法比较 key 的大小。如果 key 没有实现该接口,那么通过比较 hash 值来判定。
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();
// 获得需要树形化的链表的第一个节点 也就是数组对应的数组节点 table[i]
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
//将普通的 node 节点构造成 TreeNode 拥有更多的属性
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);
}
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;
x.left = x.right = null;
if (root == null) {
//第一次循环
//将 x 赋值给 root 节点 初始化 root 节点的颜色 黑色(红黑树的根节点 一定是黑色)
x.parent = null;
x.red = false;
root = x;
}
//此处开始构造红黑树
else {
//获得当前节点的 key 和 hash
K k = x.key;
int h = x.hash;
Class<?> kc = null;
// 从根节点开始每次遍历 寻找插入的位置
for (TreeNode<K,V> p = root;;) {
int dir, ph;
// 获得根节点的 key
//判断 key 与 root 的 key 的大小确定 dir 的值,也就确定了插入的方向 ,是 root 节点的左边还是右边
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
// 如果 key 的 hash 想等或者实现 comparable 接口又或者是实现了该接口 但是两个比较结果也相同
// 所以为了打破这种平衡必须再次调用 tieBreakOrder 方法比较一次返回值只有 -1 或 1
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
// 打破平衡的方法。
dir = tieBreakOrder(k, pk);
// 小结:如果没有实现`comparable`方法,那么比较就由 hash 值之间的大小决定
// 当且节点
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;
}
}
}
}
// 根节点目前到底是链表的哪一个节点是不确定的要将红黑树的根节点移动至链表节点的第一个位置也就是 table[i] 的位置。
moveRootToFront(tab, root);
}
总结:
- 运算尽量都用位运算,效率更高。
- 对于扩容导致需要新建数组存放更多元素时,除了要将旧数组中的元素迁移过来,也要将旧数组中的引用置 null,以便GC,否则可能会内存泄露。
- 取下标操作是用哈希值与运算 (桶的长度 - 1),即 i = (n - 1) & hash。由于桶的长度是 2 的 n 次方,这么做其实是等于 一个取模运算,但是效率更高。
- 因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即 low 位,或者扩容后的下标,即 high 位。 high 位 = 原下标 + 原哈希桶容量。
- 利用哈希值与运算旧的容量 ,e.hash & oldCap,可以通过哈希值取模后的值判断是大于等于 oldCap 还是小于 oldCap,等于 0代表小于 oldCap,应该存放在低位,否则存放在高位。这里又是一个利用位运算代替常规运算的高效点。
- 如果追加节点到链表后,当前链表节点数,不算新的节点 >= 8,则通过 treeifyBin(tab, hash); 方法将链表转化为红黑树。但是此方法内部进行了进一步判断是需要扩容还是真的转换为红黑树。
- 插入节点操作时,有一些空实现的函数,提供给 LinkedHashMap 重写。
- 在链表中插入新节点是插在尾部的,而在 JDK 1.7 的 HashMap 是插在头部。
- 整个 put 操作的流程大概如下(图片原文地址:HashMap Jdk8的实现原理)
putAll(Map<? extends K, ? extends V> m)
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);
}
将指定的 Map 中的元素添加到当前 HashMap 中,如果存在重复的 key 会替换其 value 值,细节可以看到上面的 HashMap(Map<? extends K, ? extends V> m) 构造方法。
putIfAbsent(K key, V value)
@Override
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, true);
}
重写了 Map 的默认方法,此方法只能插入不存在的 key 和 value,不会覆盖已存在的 value。
remove(Object key)
public V remove(Object key) {
Node<K,V> e;
// 1 matchValue 是false 则表示只需要 key 相等则删除
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
// 从哈希表中删除某个节点
// matchValue 是 true 则必须 key 、value 都相等才删除
// movable 是 false 则在删除节点时不移动其他节点
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
// p 是待删除节点的前节点
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 如果哈希表不为空,则根据 (n - 1) & hash 算出 index
// 如果 index 有 Node 节点
// p 指向了链表的头节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
// node 是待删除节点
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;
// 如果有 next 节点
else if ((e = p.next) != null) {
// 如果是红黑树
if (p instanceof TreeNode)
// 在红黑树中找待删除节点
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 循环遍历找到待删除节点赋给 node
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
// 找到了待删除节点
node = e;
break;
}
// p = p.next
p = e;
} while ((e = e.next) != null);
}
}
// 如果有待删除节点 node 且 matchValue 为false 或者值也相等
// 需要进行删除
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
// 2 删除红黑树中的节点
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 说明是链表头是待删除节点
else if (node == p)
// 直接将数组上的引用指向头节点的 next 节点
tab[index] = node.next;
else
// 将待删除节点前节点指向待删除节点的 next 节点
p.next = node.next;
// 结构修改 + 1
++modCount;
// 数量 - 1
--size;
// 空实现,交给 LinkedHashMap 重写
afterNodeRemoval(node);
// 返回被删除的节点
return node;
}
}
// 没有节点需要删除
return null;
}
void afterNodeRemoval(Node<K,V> p) { }
- 如果 key 存在则删除这个键值对并返回 旧的 value,如果不存在返回 null。这里也使用了 hash 函数计算 key 的哈希。
- 在 removeTreeNode 方法中如果满足对红黑树链表化的条件会进行链表化。
remove(Object key, Object value)
@Override
public boolean remove(Object key, Object value) {
// 这里传入了value 同时 matchValue 为 true
return removeNode(hash(key), key, value, true, true) != null;
}
只有指定的 key 和 value 都相等时才会删除匹配到的节点。
clear()
public void clear() {
Node<K,V>[] tab;
// 结构修改 + 1
modCount++;
// 如果 table 不是 null
if ((tab = table) != null && size > 0) {
// 更新 size 为 0
size = 0;
// 遍历
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}
可以看到 clear 操作仅仅是将数组下标对应的 Node 节点赋为 null 值,没有遍历链表和红黑树。
get(Object key)
public V get(Object key) {
Node<K,V> e;
// 使用了 hash 方法计算 key 的 hash 值,如果没有找到对应的键值对返回 null,否则返回 value 值,value 也可能是 null 值。
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;
// 查找过程和删除基本差不多,找到则返回节点,否则返回null
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);
// 如果是链表则从头节点开始遍历
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
containsKey(Object key)
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
方法内部使用 getNode 方法判断是否存在对应的 Node 节点,细节可以查看 get 方法。
containsValue(Object value)
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
if ((tab = table) != null && size > 0) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
if ((v = e.value) == value ||
(value != null && value.equals(v)))
return true;
}
}
}
return false;
}
此方法需要把每个链表和每个红黑树查找一遍直到找不到为止,时间复杂度很高。
getOrDefault(Object key, V defaultValue)
@Override
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}
如果没有找到对应的 key 则使用指定的默认值作为 value 返回。
keySet()
public Set<K> keySet() {
// 使用 AbstractMap 的 keySet成员变量
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
// KeySet 是 HashMap 的内部类,继承了 AbstractSet
final class KeySet extends AbstractSet<K> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<K> iterator() { return new KeyIterator(); }
public final boolean contains(Object o) { return containsKey(o); }
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
public final Spliterator<K> spliterator() {
return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super K> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
values()
public Collection<V> values() {
// // 使用 AbstractMap 的 keySet成员变量
Collection<V> vs = values;
if (vs == null) {
vs = new Values();
values = vs;
}
return vs;
}
// Values 是 HashMap 的内部类,继承了 AbstractCollection
final class Values extends AbstractCollection<V> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<V> iterator() { return new ValueIterator(); }
public final boolean contains(Object o) { return containsValue(o); }
public final Spliterator<V> spliterator() {
return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super V> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.value);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
entrySet()
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
// entrySet 是 HashMap 的成员变量
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
// EntrySet 是 HashMap 的内部类,继承了 AbstractSet,重写了 contains、remove、spliterator、forEach 方法
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public final Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();
}
}
遍历
通过上面三个方法可以看到 KeySet、Values、EntrySet 都分别重写了 iterator() 方法,返回了 KeyIterator、ValueIterator、EntryIterator。而他们也是 HashMap 的内部类,继承了 HashIterator 并重写了 next() 方法。
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
final class ValueIterator extends HashIterator
implements Iterator<V> {
public final V next() { return nextNode().value; }
}
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
HashIterator 是 HashMap 内部定义的抽象类。定义了对 HashMap 迭代的方法和删除方法。
abstract class HashIterator {
Node<K,V> next; // next entry to return
Node<K,V> current; // current entry
int expectedModCount; // for fast-fail
int index; // current slot
HashIterator() {
// 因为 HashMap 是线程不安全的,所以要保存 modCount 用于 fail-fast 策略
// 其他线程对 modCount 的修改这里是感知不到的
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
// next 初始化时指向哈希桶上第一个不为null的链表头
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
// 由这个方法可以看出,遍历 HashMap 的顺序是按照哈希桶从低到高,链表从前往后依次遍历的
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
// fail-fast 策略
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
// 调 HashMap 的 removeNode 方法删除节点
removeNode(hash(key), key, null, false, false);
// 修改 expectedModCount
expectedModCount = modCount;
}
}
HashMap 的线程安全问题
HashMap的线程不安全体现在会造成死循环、数据丢失、数据覆盖。其中死循环和数据丢失是在 JDK1.7 中出现的问题,在 JDK1.8 中已经得到解决,因为 JDK 1.7 采用的头插法,而 JDK 1.8 采用的尾插法,然而 JDK 1.8 中仍会有数据覆盖这样的问题。具体可参考:
JDK1.7和JDK1.8中HashMap为什么是线程不安全的?
面试题
- 为什么 loadFactors 默认是 0.75?
源码中注释,当负载因子为 0.75 的时候,桶中元素个数为 8 的概率几乎为零。通过泊松分布来看,0.75 是"空间利用率"和"时间复杂度"之间的折衷。 - 为什么默认的初始容量是 16?
因为 16 作为初始容量不会造成空间浪费和频繁扩容,是一个经验之谈。 - 为什么使用红黑树的数据结构?
因为红黑树的时间复杂度表现更好为 O(logN),而链表为 O(N)。 - 为什么红黑树这么好还要用链表?
因为大多数情况下 hash 碰撞导致的单个桶中的元素不会太多,太多也扩容了。只是极端情况下,当链表太长会大大降低 HashMap 的性能。所以为了应付这种极端情况才引入的红黑树。当桶中元素很少比如小于 8,维护一个红黑树是比较耗时的,因为红黑树需要左旋右旋操作也很耗时。红黑树在元素很少的情况下的表现不如链表。 - 为什么链表长度 > 8 但是哈希桶容量 < 64 选择的是扩容哈希桶而不是链表转红黑树?
假如容量为 16,插入了 9个元素,巧了,都在同一个桶里面,如果这时进行树化,时间复杂度会增加,性能下降,不如直接进行扩容,空间换时间。 - 为什么链表转换红黑树和红黑树转换链表的阈值不同?
想一个极端情况,假设阈值都是 8,一个桶中链表长度为 8 时,此时继续向该桶中 put 会进行树化,然后 remove 又会链表化。如果反复 put和 remove。每次都会进行极其耗时的数据结构转换。如果是两个阈值,将会形成一个缓冲带,减少这种极端情况发生的概率。上面这种极端情况也被称之为复杂度震荡。 - 为什么哈希桶的容量必须是 2 的幂?
回顾一下计算 key 在数组中的索引位的方法:hash&(n - 1),其中 n 是 HashMap 的容量也就是数组的长度。
假设 n 是奇数。则 n-1 就是偶数,偶数二进制中最后一位一定是 0。所以如上图所示,hash&(n - 1) 最终结果二进制中最后一位一定是 0,也就意味着下标一定是偶数。这会导致数组中只有偶数位被使用而奇数位白白浪费。无形中浪费了内存也增加了 hash 碰撞的概率。
2 的 n 次方一定是偶数则 n - 1 一定是奇数,这样计算的下标就可能是偶数为也可能是奇数位,避免内次浪费。
那只是偶数不行吗?为啥偏偏是 2 的 n 次方?
2 的 n 次方能保证 (n - 1) 低位都是 1,能使 hash 低位的特征得以更好的保留,也就是说当 hash 低位相同时两个元素才能产生 hash 碰撞。换句话说就是使hash更散列。
两层含义:- 从奇偶数来解释。
- 从 hash 低位的 1 能使得 hash 本身的特性更容易得到保护方面来说。(很类似源码中 hash 方法中 <<< 16 的做法)
- 为什么不直接使用 key 的 hashCode() 返回值作为哈希值?
因为 hashCode() 返回值是 int 类型,取值范围很大,而 HashMap 的哈希桶的长度远比 hash 值小,在通过取模操作找哈希桶的下标时是通过与运算完成的,一个很大的 int 和一个很小的 int 进行与运算往往会忽略 hash 值的高位,哈希碰撞率会增大。增加了扰动函数就是为了解决 hash 碰撞的。它会综合 hash 值高位和低位的特征,与运算时,使高低位一起参与运算,减少 hash 碰撞的概率。(在 JDK 1.8 之前,扰动函数会扰动四次,JDK 1.8 简化了这个操作)。 - 解决 hash 冲突的方法有哪些?
HashMap 中解决 hash 冲突采用的是链地址法,就是有冲突则在数组中将冲突的元素放到链表中。
一般有以下四种解决方案。- 链地址法
- 开放地址法
- 再哈希法
- 建立公共溢出区
- 如何保证 HashMap 的同步?
Map map = Collections.synchronizedMap(new HashMap()); 其实就是给 HashMap 的每一个方法加 Synchronized 关键字。性能远不如ConcurrentHashMap,不建议使用。 - HashMap 是先插入元素还是先扩容?
在初始化哈希桶时是先对数组进行初始化然后插入元素,否则是先插入元素在判断是否需要扩容。 - 什么是哈希表?
数组的特性是查找快,增删慢,链表的特性是增删快,查找慢,所以结合两者的特性的哈希表具备两者的优点。也就是每一个数组都是一个链表的节点,新插入的元素通过对 hash 与容量的取模操作找到对应的索引位。 - HashMap 哈希冲突时是头插法还是尾插法?
尾插法,新的节点插入到链表的尾部。 - null key 会存储在哈希桶哪个索引下?
由于 null key 的 hash 值默认是 0,而对 0 进行 hash &(n-1)运算也是 0,所以 null key 会存储在第一个索引位上。 - 如果 key 是 Object 类型需要重写哪些方法?
hashCode 方法用来计算索引位,equals 方法判断 key 是否已存在。 - 为什么建议使用 String 和 Integer 作为 key?
因为 String 和 Integer 是不可变类,并且重写了 hashCode 和 equals 方法。