1.整体架构
HashMap底层的结构是数组+链表+红黑树。当链表长度≥8且数组长度≥64时,底层代码会将链表转化为红黑树。当红黑树的节点数≤6时,底层代码又会将红黑树转化为链表。
下面展示的是节点Node和TreeNode(可以理解为链表和红黑树)的源码,这两个都是HashMap类中定义的内部类,类内部封装了对链表和红黑树的操作方法,
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;
}
......
}
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);
}
......
}
链表的添加和删除是相对简单的。当链表长度≥8时将其转化为红黑树,当转化为红黑树后,相关的添加删除节点的操作就复杂一些,主要原因是红黑树涉及到树的平衡,有左旋、右旋及改变节点颜色的操作。
至于选取数字8的原因,源码中的解释为
- 链表查询时间复杂度是 O ( n ) O(n) O(n),红黑树的查询时间复杂度是 O ( l o g ( n ) ) O(log(n)) O(log(n))。当链表数据量不大时,其遍历速度也是比较快的。红黑树虽然查询速度快,但是其内存占用是链表的两倍,考虑时间和空间的损耗,选取8作为一个边界值。
- 选取数字8的过程参考了泊松分布概率函数。当链表长度为8时,出现的概率不到千万分之一,所以正常情况下链表长度很少有达到8的时候。
2.源码解析
类属性
// 默认的初始化容量 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量,未达到该值时,HashMap的容量始终是2的整数次幂
static final int MAXIMUM_CAPACITY = 1 << 30;
// 负载因子默认值
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表长度大于等于该值将会被转化为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 红黑树节点数小于等于该值则被转化为链表
static final int UNTREEIFY_THRESHOLD = 6;
// 数组容量大于等于该值时,链表才会转化为红黑树。否则每次扩容都是对数组进行resize操作
static final int MIN_TREEIFY_CAPACITY = 64;
HashMap类还有如下需要关注的信息,
- 允许null作为值存在
- HashMap是线程不安全的,使用Collections.synchronizedMap可以得到全部方法上锁的线程安全的Map对象
- 负载因子的默认值是0.75,是军和了时间和空间损耗计算出来的。
不扩容的条件,
数 组 容 量 > 需 要 的 数 组 大 小 l o a d _ f a c t o r 数组容量>\frac{需要的数组大小}{load\_factor} 数组容量>load_factor需要的数组大小
当load factor较大,会减少空间开销(数组扩容次数减少,数组大小增速缓慢),增加了查找成本(hash冲突增加,链表长度或红黑树变得庞大)。 - 如果有很多数据存储到HashMap中,建议HashMap的容量一开始就设置成足够的大小,这样可以防止在插入过程中不断扩容,影响性能。
这些类属性对应了不同的HashMap的构造方法,
# 无参数初始化
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
# 指定容量初始化
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
# 指定大小和负载因子初始化
public HashMap(int initialCapacity, float loadFactor) {
......
}
添加键值对
HashMap添加键值对使用的是put方法,每个键值对在底层是链表的一个Node对象,或者是红黑树的一个TreeNode对象。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
// hashCode是Object类的底层方法
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
增添的步骤如下,
- 数组有无初始化,如果没有则首先进行数组的初始化
- 得到键值key的哈希值,如果发现key值已经存在,则跳转到第6步
- 如果hash冲突,解决方案有两种:链表或红黑树
- 如果是链表,递归将Node添加到链表尾部
- 如果是红黑树,使用红黑树的插入节点方法将TreeNode放到树上
- 通过4,5将新元素追加成功后,依据参数onlyIfAbsent(见源码)判断是否需要覆盖
- 判断是否需要扩容,如果需要则进行扩容
下图是对上面步骤的总结,
putVal方法是HashMap添加键值对过程中最关键的方法,
/**
* 对Map的put方法的实现
* @param hash key的hash值
* @param key key值
* @param value value值
* @param onlyIfAbsent if true, don't change existing value
* @param evict 如果是false,HashMap处在creation模式.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
// tab相当于底层数组
// p相当于数组某个索引位置上的节点对象(链表头或红黑树root)
// n是数组的容量,i是本次插入节点对应的数组索引
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果数组没有初始化,则首先进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 通过一步二进制位与运算得到本次插入值在数组中的索引位置
// 如果目前该位置没有节点(null),则将当前插入的键值对应的Node对象放在该位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 如果数组该位置上有节点,变量e是临时变量
Node<K,V> e; K k;
// 如果p的key和本次插入的key的hash和值均相等,则直接将p赋值给e
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果p与本次插入的值不相等且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);
// 如果链表长度binCount+1大于8(binCount时索引),则转为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 自旋过程中出现与此次插入的key相同的节点,循环结束
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// p在自旋过程中一直往后移动
p = e;
}
}
// 这一个判断为true说明链表头部或链表中存在与新插入的key相同的节点
// 需要对原来的key进行更新
if (e != null) {
V oldValue = e.value;
// 当onlyIfAbsent为false时,才会覆盖值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 记录版本信息,如果数组的大小大于阈值则进行扩容
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
上面的代码中,确定插入的键值对在数组中的索引位置使用的是下面一行计算代码,
(n - 1) & hash
其中,n是数组长度,hash是此次插入的key的hash值。这种确定索引位置的好处是,
- 不会发生数组越界
- 保证元素尽可能的均匀分布
链表新增节点过程
在putVal方法中直接展示了链表自旋方式插入新节点的过程,
for (int binCount = 0; ; ++binCount) {
// 自旋到队尾,放到链表尾端
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 如果链表长度binCount+1大于8(binCount时索引),则转为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 自旋过程中出现与此次插入的key相同的节点,循环结束
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// p在自旋过程中一直往后移动
p = e;
}
当链表插入节点后,如果发现链表的长度大于8,则会调用HashMap类的treeify方法,该方法会通过判断链表长度和数组长度决定将链表转换为红黑树或者使用resize方法进行扩容。
红黑树新增节点过程
putVal方法中,如果判断数组中某一索引位置上的元素非null且属于TreeNode类型,此时会调用TreeNode类的putTreeVal方法在这棵红黑树上添加该节点。
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
该方法返回红黑树插入新节点后的root节点,具体源码如下,
// h是新增节点key的hash值
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab, int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
// 获取根节点root
TreeNode<K,V> root = (parent != null) ? root() : this;
// 自旋
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
// p的hash值>h,说明p在h的右侧
if ((ph = p.hash) > h)
dir = -1;
// 反之,在左侧
else if (ph < h)
dir = 1;
// 如果新增的key已经存在于红黑树中,则直接返回该节点
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//如果自定义实现的Comparable的话,不能用hashcode比较了,需要用compareTo
else if ((kc == null &&
// key的类没有实现Comparable返回null
(kc = comparableClassFor(k)) == null) ||
// 当前节点p的key值与新增节点不相等
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
// 递归在子树中寻找,如果找到说明该key值已存在于红黑树中
// 如果找到返回相应的节点
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);
}
// 运行到此处说明红黑树中不存在与新增key值相同的节点
TreeNode<K,V> xp = p;
// 找到和当前节点hash值相近的节点(当前节点的左右子节点为空即可)
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;
// balanceInsertion对红黑树进行再平衡
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
上述过程可以总结为如下步骤,
- 首先判断新增节点的key是否已经存在,方式有两种:
1.1 如果节点类没有实现Comparable接口,使用equals进行判断
1.2 如果节点类实现了Comparable接口,使用CompareTo进行判断 - 如果新增的key已经存在于红黑树上,直接返回该节点,在putVal方法中对原始的值进行覆盖;不存在则判断新增节点位于当前节点的左边还是右边,新增节点hash值小在左子树;反之,在右子树
- 自旋递归步骤1和2,知道当前节点的左边或者右边为空时,停止自旋,当前节点即为新增节点的父节点
- 把新增节点放到当前节点的相应位置,并建立父子关系和前后关系。
- 插入新节点后对红黑树进行重新平衡。
链表与红黑树间相互转换
链表转红黑树
putVal方法中,当链表尾部成功插入一个节点Node后,会检查链表的长度是否超过设定的阈值8。如果超过则会调用HashMap类的treeifyBin方法判断对数组进行扩容或者将链表转为红黑树,
/**
* 将链表转换为红黑树,如果数组长度小于MIN_TREEIFY_CAPACITY则用resize方法扩容
*/
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();
// 转换为红黑树,首先获取红黑树的root节点e
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
// 循环将链表的节点都转换为树节点,并记录表头和表尾hd和tl
do {
// replacementTreeNode方法将Node对象转换为TreeNode对象
TreeNode<K,V> p = replacementTreeNode(e, null);
// 链表第一个Node节点初始化为TreeNode记为hd
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
// tl是链表最后一个节点
tl = p;
} while ((e = e.next) != null);
// 数组该索引位上放置hd,之后建树
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
treeifyBin方法中确定了表头节点hd后,将其转换为TreeNode,并调用TreeNode类的成员方法treeify构建红黑树。
红黑树转链表
红黑树转换链表使用的是TreeNode类中的split方法,每次HashMap进行扩容(即调用resize方法)时,都会在内部进行判断是否执行split方法对数组某个索引位置上的红黑树进行简化。
split方法的注释如下,
Splits nodes in a tree bin into lower and upper tree bins, or untreeifies if now too small. Called only from resize;
该方法的用途有两种,
- 将一颗红黑树拆分成lower和upper两棵树
- 如果红黑树过小,则使用untreeify方法将其拆成链表
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
// b是当前节点,lo和hi分别是拆分得到的low和high两棵树
TreeNode<K,V> b = this;
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
// 循环过程,e表示循环到的节点
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;
}
}
// low和high不为空的情况下,通过判断红黑树大小决定是否调用untreeify方法转化为链表
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
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);
}
}
}
该方法中调用了TreeNode类的untreeify方法,该方法源码如下,
/**
* Returns a list of non-TreeNodes replacing those linked from this node.
*/
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
// replacementNode方法将TreeNode转为Node,依据构建红黑树时指定的前后关系还原链表
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
查找
HashMap的查找过程如下,
final Node<K,V> getNode(int hash, Object key) {
// tab是底层数组,first是数组某索引位置上的Node对象(链表头或红黑树root)
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 通过key确定索引位置,并检查first节点
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;
// 如果是TreeNode对象,则调用红黑树的getTreeNode方法;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 如果是链表则自旋查找
do {
// 遍历链表判断key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
- 依据hash算法确定查找的key在数组中的索引位置
- 判断该位置上的节点对象是否存在next节点,如果存在则是链表类型;反之,是红黑树
- 分别走链表和红黑树两种不同的查找方法
链表查找方法
将上面代码的自旋过程。
红黑树查找
红黑树查找相对比较麻烦,会有专门的笔记对源码进行解析。具体查找过程可以总结为,
- 从根节点递归查找
- 依据查询的key的hash值查找节点,与当前节点比较,如果比当前节点小,则在左子树中查询;反之,若大于当前节点hash值,则在右子树中查询。如果相等,则返回该节点。
- 自旋至无法继续迭代,如果没有定位到则说明没有该key值。
如果红黑树比较平衡的话,每次查找的最大次数就是树的深度。