Java源码解读系列13—HashMap(JDK1.8 )
1 概述
- JDK7的HashMap底层采用数组+链表数据结构进行存储,会存在一个问题,即当解决哈希碰撞的链表过长的时候,HashMap的查询效率就会变低。红黑树是平衡二叉树(黑色平衡),其查询效率为O(lgn),优于链表的查询效率O(n)。 因此JDK8的HashMap底层采用数组+链表+红黑树数据结构进行存储。
- 具体过程如下, 1)每次新增一个数据,就会被插入到数组中的空位。慢慢的数据多了,就会存在哈希碰撞问题,即2个key的索引一样。HashMap采用的解决方法是链地址法。数组中每个元素设置链表。key映射到数组某个元素中,而数据项本身插入到元素的链表中。2)当链表中的节点个数大于8时,链表会转变为红黑树3)当链表的节点个数小于等于6时,红黑树又会转变为链表
- JDK8的HashMap中使用大量红黑树的方法,本文重点放在HashMap本身,具体红黑树的增删查改以及红黑平衡等特性均不做深入探讨。
2 构造函数
2.1 无参构造函数
JDK7的HashMap的无参构造函数初始化时候,会给存储的数组初始化并赋予默认大小16。JDK8已经不将存储的数组的大小初始化为默认值16,而是留到第一次put操作时对存储数组大小扩容到默认值16.
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
2.2 有参构造函数
这里虽然指定了初始化容量,但是初始化后存储数组的大小依然为0,依然留到第一次put操作的时候才会进行扩容。但这里会改变数组扩容的阀值threshold为大于initialCapacity的最小2的n次方。
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
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);
}
3 节点类型
3.1 链表的节点
Node类型是构成链表的节点
static class Node<K,V> implements Map.Entry<K,V> {
//哈希值
final int hash;
//存储K-V结构的key值
final K key;
//存储K-V结构的value值
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;
}
...
}
3.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);
}
...
}
4 添加数据
4.1 put方法
直接调用putVal方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
4.2 hash方法
获取key的哈希值
static final int hash(Object key) {
int h;
//h >>> 16:无符号右移动16位
//h和h >>> 16做了异或操作
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
4.3 putVal方法
通过 (n - 1) & hash定位到新增的key在数组中的哪个位置上
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判断table数组是为为空或者长度为0
if ((tab = table) == null || (n = tab.length) == 0)
//对table进行扩容
n = (tab = resize()).length;
//判断待插入位置上的节点p是否为空
if ((p = tab[i = (n - 1) & hash]) == null)
//创建一个新的节点
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//判断待插入位置上的节点p的key是否和待插入的key相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//将p赋值给e
e = p;
//判断待插入位置上的节点p是否为类TreeNode的一个实例
else if (p instanceof TreeNode)
//调用红黑树的putTreeVal方法添加数据
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//无限循环,直到找到匹配条件
for (int binCount = 0; ; ++binCount) {
//寻找p的下一个为空的节点
if ((e = p.next) == null) {
//将p的next引用指向新创建的节点
p.next = newNode(hash, key, value, null);
//树化的条件之一是链表的节点数 > TREEIFY_THRESHOLD
//这里为什么TREEIFY_THRESHOLD要减去1?这是因为binCount为0时,其实已经是链表的第2个节点。也就是说满足binCount >= TREEIFY_THRESHOLD - 1 = 8 -1 = 7的条件时,链表的节点数至少为9个
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//调用红黑树的树化方法
treeifyBin(tab, hash);
//跳出循环
break;
}
//p的下一个节点e非空,则判断e的key是否和待插入的key相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//跳出循环
break;
//将e赋值给p,走到这一步说明又要开始遍历for循环
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//钩子方法,留给实现类做拓展,比如LinkedHashMap。
afterNodeAccess(e);
return oldValue;
}
}
//更新修改次数
++modCount;
//执行添加操作后,HashMap中节点数size执行加1操作
//如果size超过阀值threshold,调用resize()方法扩容
if (++size > threshold)
resize();
//钩子方法
afterNodeInsertion(evict);
//返回空
return null;
}
4.4 treeifyBin方法
索引位置为 (n - 1) & hash上的链表的节点数大于8且数组的容量大于64时,会将链表转换为红黑色。这里需要注意的是不是将数组上所有的元素都转换为红黑树,而是仅仅转换索引位置为 (n - 1) & 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();
//判断索引位置为index = (n - 1) & hash上的元素是否为空
else if ((e = tab[index = (n - 1) & hash]) != null) {
//使用一个链表存储红黑树的节点,其中hd指向链表头,tl指向链表尾节点
TreeNode<K,V> hd = null, tl = null;
do {
//调用replacementTreeNode,通过e节点新生产一个红黑树节点p
TreeNode<K,V> p = replacementTreeNode(e, null);
//首次遍历,尾节点为空
if (tl == null)
//将hd指向p
hd = p;
else {
//保留p上一个节点的信息
//将p的头引用指向tl
p.prev = tl;
//将tl的下一个引用指向p
tl.next = p;
}
//将尾节点的引用tl指向p
tl = p;
//判断e的下一个节点是否为空
} while ((e = e.next) != null);
//将hd赋值给数组index位置上的元素
//判断hd是否为空
//若不为空,因为hd -> .. ->tl目前是通过链表的形式保存上下节点的信息,调用treeify方法将链表转换为红黑树
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
4.5 replacementTreeNode方法
使用链表类型的p节点构建一个红黑树节点
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
5 查询
5.1 get方法
通过key查询数据,直接调用getNode方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
5.2 getNode方法
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//判断数组是否为空,数组长度是否为0,数组(n - 1) & hash位置上的元素first是否为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//判断first的key是否和待获取的key相等
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
//返回数组指定位置的首个元素
return first;
//判断下一个元素是否为空
if ((e = first.next) != null) {
//判断first节点是否为红黑树节点类型的一个实例
if (first instanceof TreeNode)
//调用红黑树getTreeNode方法进行查找
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//first节点是链表类型的一个实例
//循环遍历找到某元素的哈希值和key与待获取的哈希值和key都相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//找不到与待获取的key相等的元素,返回空
return null;
}
6 删除
6.1 remove方法
remove方法删除元素实际调用removeNode方法
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
6.2 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;
//判断数组是否为空,数组长度是否为大于0,数组(n - 1) & hash位置上的元素p 是否为空
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;
//判断p的哈希值是否与待删除的hash值相
//判断p的key是否和待删除的key相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//将p赋值给node
node = p;
//判断p的下一个节点e是否为空
else if ((e = p.next) != null) {
//判断p是否为红黑树节点类型TreeNode的一个实例
if (p instanceof TreeNode)
//调用红黑树的getTreeNode方法进行查找
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
//p节点是链表类型的一个实例
//循环遍历找到某元素的哈希值和key与待删除的哈希值和key都相等
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
//找到符合条件的节点
node = e;
//跳出循环
break;
}
//将e赋值给p,主要是了保存待删除的节点e的上一个节点的信息
p = e;
} while ((e = e.next) != null);
}
}
//待删除节点node不为空
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 判断node是否为红黑树节点类型TreeNode的一个实例
if (node instanceof TreeNode)
//调用红黑树的removeTreeNode方法进行删除
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//node节点是链表类型的一个实例
//node等于p,说明node位于链表的首位
else if (node == p)
//将node的下一个元素赋值给数组index位置
tab[index] = node.next;
else
//node不为链表的首位,将node的上一个节点的引用指向node的下一个节点,是否node节点
p.next = node.next;
//修改次数加1
++modCount;
//节点总数减1
--size;
//钩子方法
afterNodeRemoval(node);
//返回node
return node;
}
}
//找不到待删除元素,返回空
return null;
}
7 扩容
JDK7的HashMap的无参构造函数初始化时候,会给存储的数组初始化并赋予默认的的大小。JDK8是在首次添加的时,通过resize方法给将数组的扩容到大小到16。
7.1 resize方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//旧的数组容量,首次进来时oldTab为空,即oldCap为0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//旧的扩容阀值
int oldThr = threshold;
//新的数组容量和扩容阀值
int newCap, newThr = 0;
//判断旧的数组容量是否大于0
if (oldCap > 0) {
//旧的数组容量大于等于MAXIMUM_CAPACITY
if (oldCap >= MAXIMUM_CAPACITY) {
//将扩容阀值设为0x7fffffff
threshold = Integer.MAX_VALUE;
//返回旧的数组
return oldTab;
}
//新的数组容量为旧的左移一位
//新的数组容量小于MAXIMUM_CAPACITY且旧的数组容量大于等于DEFAULT_INITIAL_CAPACITY
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
//使用带指定初始化容量的构造函数,首次调用resize方法时,oldThr不为0但是oldCap为0
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 使用无参构造函数,首次调用resize方法时oldThr和oldCap的值都为0
//newCap和newThr重新赋值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//新的容量为0时
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
//判断newCap和ft是否小于MAXIMUM_CAPACITY
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;
//判断oldTab是否为空,若不为空,则将旧数组中的值复制到新的数组中
if (oldTab != null) {
//遍历旧的数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//判断oldTab[j]是否为空,不为空则进行复制
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//如果e.next为空,说明该位置的链表上只有一个元素
if (e.next == null)
//将e赋值到新数组的e.hash & (newCap - 1)位置上
newTab[e.hash & (newCap - 1)] = e;
//e.next不为空且e的节点类型属于红黑树
else if (e instanceof TreeNode)
//调用红黑树的split进行复制
//这里入参的this是指当前对象,即当前的HashMap实例
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//e.next不为空且e的节点类型属于链表
//使用链表方法进行复制
else { // preserve order
//构建2条不同的链表,XXHead和XXTail分别是头引用和尾引用
//其中一条lo链表上的值存储到的新数组j位置上,也就是和旧的数组上的位置一致,称为旧的索引位置上的链表
//另外一条hi链表上的值存储的新数组j+oldCap位置上,称为旧的索引+oldCap位置上链表
//lo开头的链表上面的数值存储位置和旧的数组上的位置一致
//loHead指向链表首位,loTail指向链表末尾
Node<K,V> loHead = null, loTail = null;
//hi开头的链表上面的数值存储位置为旧的数组上的位置j加上oldCap
//hiHead指向链表首位, hiTail指向链表末尾
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//循环遍历链表,直到e的下一个节点为空
do {
next = e.next;
//判断e.hash & oldCap的值是否为0,若为0则存储位置和旧的数组上的位置一致
if ((e.hash & oldCap) == 0) {
//首次进来loTail为空
if (loTail == null)
//将loHead指向lo链表首个节点
loHead = e;
else
//loTail的下一个节点指向满足条件的节点e
loTail.next = e;
//将loTail指向lo链表的尾节点
loTail = e;
}
else {
//首次进来hiTail为空
if (hiTail == null)
//将lhiHead指向hi链表首个节点
hiHead = e;
else
//hiTail的下一个节点指向满足条件的节点e
hiTail.next = e;
//将loTail指向lo链表的尾节点
hiTail = e;
}
} while ((e = next) != null);
//判断lo链表是否为空
if (loTail != null) {
//将loTail的next引用赋值为null
loTail.next = null;
//将lo链表的头节点赋值给新数值的j位置
newTab[j] = loHead;
}
//判断hi链表是否为空
if (hiTail != null) {
//将hiTail的next引用赋值为null
hiTail.next = null;
//将lhi链表的头节点赋值给新数值的j + oldCap位置
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//返回新的数组
return newTab;
}
7.2 split方法
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
//split方法是树叶红黑树TreeNode中的方法,因此这里的this是红黑树节点的实例
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
//构建2条不同的链表,XXHead和XXTail分别是头引用和尾引用
//其中一条lo链表上的值存储到的新数组j位置上,也就是和旧的数组上的位置一致,称为旧的索引位置上的链表
另外一条hi链表上的值存储的新数组j+oldCap位置上,称为旧的索引+oldCap位置上链表
//lo开头的链表上面的数值存储位置和旧的数组上的位置一致
//loHead指向链表首位,loTail指向链表末尾
TreeNode<K,V> loHead = null, loTail = null;
//hi开头的链表上面的数值存储位置为旧的数组上的位置j加上oldCap
//hiHead指向链表首位, hiTail指向链表末尾
TreeNode<K,V> hiHead = null, hiTail = null;
//lc 和hc分别统计两种不同链表中链节点的个数
//lc为旧的索引位置上链表节点的个数
//hc为旧的索引+oldCap位置上链表节点的个数
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;
//判断e.hash & oldCap的值是否为0,若为0则存储位置和旧的数组上的位置一致
if ((e.hash & bit) == 0) {
//首次进来时loTail为空
//将loTail的值赋予e的前驱节点
if ((e.prev = loTail) == null)
//将e赋值给loHead
loHead = e;
else
//将loTail的next引用指向e
loTail.next = e;
//将e赋值给loTail
loTail = e;
//旧的索引位置上链表节点的个数加1
++lc;
}
else {
//首次进来时hiTail为空
//将hiTail的值赋予e的前驱节点
if ((e.prev = hiTail) == null)
//将e赋值给hiHead
hiHead = e;
else
//将hiTail.的next引用指向e
hiTail.next = e;
//将e赋值给hiTail
hiTail = e;
//旧的索引+oldCap位置上链表节点的个数加1
++hc;
}
}
//旧的索引位置上的链表不为空
if (loHead != null) {
//如果旧的索引位置上的链表节点数小于等于6
if (lc <= UNTREEIFY_THRESHOLD)
//调用untreeify方法将红黑树转换为链表 ,并将链表头节点赋值给tab[index]
tab[index] = loHead.untreeify(map);
else {
//将旧的索引位置上的链表头节点赋值给 tab[index]
tab[index] = loHead;
//如果旧的索引+oldCap位置上链表的头节点不为空,说明
红黑树的节点分别位于两条链表上
if (hiHead != null) // (else is already treeified)
//将旧的索引位置上的链表重新树化
loHead.treeify(tab);
}
}
// 旧的索引+oldCap位置上链表的头节点不为空
if (hiHead != null) {
//如果旧的索引+oldCap位置上的链表节点数小于等于6
if (hc <= UNTREEIFY_THRESHOLD)
//调用untreeify方法将红黑树转换为链表 ,并将链表头节点赋值给tab[index+ bit]
tab[index + bit] = hiHead.untreeify(map);
else {
//将旧的索引+oldCap位置上的链表头节点赋值给 tab[index+ bit]
tab[index + bit] = hiHead;
//如果旧的索引位置上链表的头节点不为空,说明
红黑树的节点分别位于两条链表上
if (loHead != null)
//将旧的索引+oldCap位置上的链表重新树化
hiHead.treeify(tab);
}
}
}
7.3 untreeify
调用untreeify将红黑树转换为链表
final Node<K,V> untreeify(HashMap<K,V> map) {
//hd为链表头引用,tl为链表尾节点引用
Node<K,V> hd = null, tl = null;
//循环遍历直到q的值为空
for (Node<K,V> q = this; q != null; q = q.next) {
//获取链表类型的节点p
Node<K,V> p = map.replacementNode(q, null);
//首次进来,tl为空
if (tl == null)
//将p赋值给hd
hd = p;
else
//非首次进来,将tl的next引用指向p
tl.next = p;
//将p赋值给tl
tl = p;
}
//返回头引用
return hd;
}
7.4 replacementNode
通过红黑树节点的哈希值和key,生成一个新的链表节点
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
return new Node<>(p.hash, p.key, p.value, next);
}
8 参考文献
1)JDK7在线文档
https://tool.oschina.net/apidocs/apidoc?api=jdk_7u4
2) JDK8在线文档
https://docs.oracle.com/javase/8/docs/api/
3) Bruce Eckel. Java编程思想,第4版,2007,机械工业出版社
4)方腾飞,魏鹏,程晓明. Java并发编程的艺术,第1版,2015年,机械工业出版社