JDK8以前HashMap的实现是数组加链表,JDK8以后实现是数组加链表加红黑树。
先说一下红黑树的几个必要条件
一、 put操作源码解析
1. hash(key) 对key进行hash操作
//先对key进行hash,然后调用putVal方法
//key的hashCode高位和地位进行异或操作
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
为什么要使用高低位取异或来当key的hash值?
从源码可以看到,取数组下标的运算为 i = (n - 1) & hash
高位和低位异或使得hash值更加均匀,这样计算存放下标时就避免更多的hash碰撞。
2. put操作核心代码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
//判断table是否为null,为null则调用resize()进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//判断槽位是否为null,如果为空,则直接新建一个node放在数组的i位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//如果槽位不为空,则说明发声hash碰撞
Node e; K k;
//如果hash值相同,key也相同,则说明key对应的值更新了,直接当前p赋值给e,在后续进行更新操作
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果当前找到的链表p是TreeNode 红黑树,则以TreeNode的方式将数据插入红黑树
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
//遍历链表p
for (int binCount = 0; ; ++binCount) {
//p的后继节点为空,则将新建一个节点,并将p的后继节点指向新建的节点
if ((e = p.next) == null) {
//将新建一个节点,并将p的后继节点指向新建的节点
p.next = newNode(hash, key, value, null);
//如果节点的个树超过设置的数组的阈值,则进行扩容操作
if (binCount >= TREEIFY_THRESHOLD - 1)
//转化为红黑树结构
treeifyBin(tab, hash);
break;
}
//如果hash值相同,key也相同,则说明key对应的值更新了
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//e不为空进行更新操作
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//modCount自增
++modCount;
//如果当前size+1大于数组的阈值,则进行数组扩容操作
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
4. resize()源码解析
final Node[] resize() {
//获取当前的数组,数组的长度和当前的阈值
Node[] 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;
}
//将新的容量大小左移一位设置为原来容量的两倍
//如果新的容量小于最大容量,并且原来容量要大于等于初始容量,则将阈值设置为原来的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
//如果原来的阈值大于0,则将容量设置为原来的阈值
else if (oldThr > 0)
newCap = oldThr;
else {
//容量和阈值设置为默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果新的阈值为0
if (newThr == 0) {
//新的容量*阈值系数0.75
float ft = (float)newCap * loadFactor;
//如果新的容量小于最大容量并且重新计算的阈值也小于最大容量,则将新阈值赋值为ft,否则为int最大值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//将新阈值赋值给成员变量
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//以新的容量创建一个node数组,并将其赋值给成员变量table
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
//如果以前的数组不为null,i = (n - 1) & hash为数组下标,n为数组长度,因为数组的长度变了,所以需要将原来的数组下得链表或者红黑树重新排列。
if (oldTab != null) {
//遍历原来的table
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//如果e后继为null,则直接把e根据下标计算e.hash&(newCap-1)赋值到新的数组中
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果e为红黑树
else if (e instanceof TreeNode)
((TreeNode)e).split(this, newTab, j, oldCap);
else {
//如果e有后继节点
//低位头节点,尾结点
//高位头节点,尾结点
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {
next = e.next;
//因为数组的容量都为2的幂次方,2^n次幂二进制表示中最高位为1,如果要hash&2^n=0的话则只要hash的二级制不为1即可,所以 (2oldCap-1)与(oldCap-1)的地低都是1,假设oldCap为2^4,则(oldCap-1)二进制为01111 (2oldCap-1)二进制为 011111,那么hash&(oldCap-1)必然和hash&(2oldCap-1)相等
//所以(e.hash & oldCap) == 0用来判断坐标是否改变了,没改变的用低位节点,改变了用高位节点
//将链表分为高低位链表,使用尾插法创建新的链表
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
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;
//j + oldCap = hash&(2oldCap -1) 这个公式可以验证一下
//假设旧的hash=7二进制为0111
//oldCap为16,二进制为10000,2oldCap为32,二进制为100000
//原来的坐标二进制为10110,新的坐标为100110,
//而j+oldCap =10110+10000=100110,即新的坐标为旧的坐标+旧的容量
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
5. resize()方法中如果节点为红黑树,调用split方法,传入参数为当前map对象,新的数组对象,下标,旧的数组容量
final void split(HashMap map, Node[] tab, int index, int bit) {
TreeNode b = this;
// 将以前的红黑树分为高低位两个树
//低位头节点loHead ,低位尾结点loTail
TreeNode loHead = null, loTail = null;
//高位头节点hiHead ,高位尾结点hiTail
TreeNode hiHead = null, hiTail = null;
//lc表示低位树的节点个数,hc表示高位树节点个数
int lc = 0, hc = 0;
//循环遍历,并把各个节点先组成链表
for (TreeNode e = b, next; e != null; e = next) {
next = (TreeNode)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;
}
}
//如果低位的头节点不为null
if (loHead != null) {
//如果低位节点个数不大于树化阈值6,则将当前的map untreeify
if (lc <= UNTREEIFY_THRESHOLD)
//untreeify()方法返回TreeNode对象链表转成Node对象的链表
tab[index] = loHead.untreeify(map);
else {
//如果满足树化条件,则当前数组下标指向低位头节点
tab[index] = loHead;
//如果满足树化条件,高位头节点不为null,则树化当前
if (hiHead != null)
//低位头节点树化treeify
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);
}
}
}
6. untreeify(map)方法
//该方法就是将每个节点的TreeNode对象转成Node对象,最后返回Node对象链表
final Node untreeify(HashMap map) {
Node hd = null, tl = null;
for (Node q = this; q != null; q = q.next) {
Node p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
Node replacementNode(Node p, Node next) {
return new Node<>(p.hash, p.key, p.value, next);
}
7. treeify(map)方法
final void treeify(Node[] tab) {
TreeNode root = null;
// 循环遍历链表
for (TreeNode x = this, next; x != null; x = next) {
next = (TreeNode)x.next;
x.left = x.right = null;
//第一次将当前节点赋值给根节点
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class> kc = null;
//遍历树,一个一个添加子节点到root节点
for (TreeNode p = root;;) {
int dir, ph;
K pk = p.key;
//这里判断添加到父节点的左边还是右边
//比较hash大小
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
//如果实现了comparable接口并且compare的返回值为0
//但是又必须要确定是左子节点还是右子节点,就是用tieBreakOrder方法
dir = tieBreakOrder(k, pk);
TreeNode 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;
}
}
}
}
//因为红黑树可能会左旋或者右旋导致root节点变化,所有要将数组中的节点指向root节点
moveRootToFront(tab, root);
}
8. balanceInsertion(root,x) 调整红黑树方法
/**
* 插入平衡(分多钟情况,左旋,右旋)
* @param root 当前根节点
* @param x 当前要插入的节点
* @return 返回根节点(平衡涉及左旋右旋会将根节点改变,所以需要返回最新的根节点)
*/
static TreeNode balanceInsertion(TreeNode root,TreeNode x) {
//首先将要插入的节点染色成红色,不要问为什么不是黑色,因为黑色会破坏黑高(黑色完美平衡),还是不知道到先去学一下红黑树的性质再来看
x.red = true;
//xp:x的父节点,xpp:x的爷爷节点,xppl:爷爷节点的左子节点,xppr:爷爷节点的右子节点
//死循环,直到找到根节点才结束。
for (TreeNode xp, xpp, xppl, xppr;;) {
//如果当前插入节点的父节点为空,那么说明当前节点就是根节点
if ((xp = x.parent) == null) {
//染色为黑色(根节点规定为黑色)
x.red = false;
return x;
}
//如果父节点为黑色或者爷爷节点为空(插入后不影响黑色完美平衡,直接返回)
else if (!xp.red || (xpp = xp.parent) == null)
return root;
//当前插入节点的父节点为红色,并且是爷爷节点的左子节点(有两种情况:LL或者LR)
if (xp == (xppl = xpp.left)) {
//叔叔节点存在并且为红色
if ((xppr = xpp.right) != null && xppr.red) {
//将父节点和叔叔节点染色成黑
xppr.red = false;
xp.red = false;
//将爷爷节点染色成红
xpp.red = true;
//最后将爷爷节点设置为当前节点进行下一轮操作
x = xpp;
}
//叔叔节点不存在或者为黑色
else {
// 当前插入节点是父节点的右子节点(LR的情景)
if (x == xp.right) {
//以父节点为旋转节点进行左旋
root = rotateLeft(root, x = xp);
//设置爷爷节点
xpp = (xp = x.parent) == null ? null : xp.parent;
}
//左旋完了之后,就回到了LL的情景(父节点是爷爷节点的左子节点,当前节点是父节点的左子节点),然后父节点又是红色,当前插入节点也是红色,违反了红黑色的性质,红色不能两两相连,所以接下来需要进行染色;
if (xp != null) {
//将父节点染色为黑
xp.red = false;
if (xpp != null) {
//将爷爷节点染色为红,然后在对爷爷节点右旋。
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
//父节点是爷爷节点的右子节点,父节点为红色,也有两种情况(RR 或者 RL)
else {
//叔叔节点不为空并且为红色
if (xppl != null && xppl.red) {
//需要将父节点和叔叔节点染色成黑将爷爷节点染色成红
xppl.red = false;
xp.red = false;
xpp.red = true;
//并且爷爷节点设置为当前节点进行下一轮操作
x = xpp;
}
//叔叔节点不存在或者为黑色
else {
//当前插入节点是父节点的左子节点(RL的情景)
if (x == xp.left) {
//先将父节点右旋变成的RR的情景
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
//这个时候已经变成了RR的情况,需要染色在意爷爷节点左旋来维持平衡
if (xp != null) {
//将父节点染色为黑
xp.red = false;
if (xpp != null) {
//将爷爷节点染色成红
xpp.red = true;
//再对爷爷节点左旋
root = rotateLeft(root, xpp);
}
}
}
}
}
}
9. 左旋
/**
* 左旋
* @param root 当前根节点
* @param p 指定的旋转节点
* @return 返回根节点(平衡涉及左旋右旋会将根节点改变,所以需要返回最新的根节点)
* 左旋示意图:左旋p节点
pp pp
| |
p r
/ \ ----> / \
l r p rr
/ \ / \
rl rr l rl
左旋做了几件事?
* 1、将rl设置为p的右子节点,将rl的父节点设置为p
* 2、将r的父节点设置pp,将pp的左子节点或者右子节点设置为r
* 3、将r的左子节点设置为p,将p的父节点设置为r
*/
static TreeNode rotateLeft(TreeNode root,TreeNode p) {
// r:旋转节点的右子节点; pp:旋转节点的父节点;rl:旋转节点的右子节点的左子节点
TreeNode r, pp, rl;
//旋转节点非空并且旋转节点的右子节点非空
if (p != null && (r = p.right) != null) {
//将p节点的右子节点设置为右子节点的左子节点
if ((rl = p.right = r.left) != null)
//将rl的父节点设置为p
rl.parent = p;
//将r的爸爸节点设置为p的爸爸节点,如果是空的话
if ((pp = r.parent = p.parent) == null)
//染色成黑
(root = r).red = false;
//判断父节点是爷爷节点的左子节点还是右子节点
else if (pp.left == p)
//如果是左子节点,那么就把爷爷节点的左子节点设置为r
pp.left = r;
else
//如果是右子节点,就把爷爷节点的右子节点设置为r
pp.right = r;
//最后将r的左子节点设置为p
r.left = p;
//将p的爸爸节点设置为r
p.parent = r;
}
return root;
}
10. 右旋
/**
* 右旋
* @param root 当前根节点
* @param p 指定的旋转节点
* @return 返回根节点(平衡涉及左旋右旋会将根节点改变,所以需要返回最新的根节点)
* 右旋示意图:右旋p节点
pp pp
| |
p l
/ \ ----> / \
l r ll p
/ \ / \
ll lr lr r
* 右旋都做了几件事?
* 1.将lr设置为p节点的左子节点,将lr的父节点设置为p
* 2.将l的父节点设置为pp,将pp的左子节点或者右子节点设置为l
* 3.将l的右子节点设置为p,将p的父节点设置为l
*/
static TreeNode rotateRight(TreeNode root,TreeNode p) {
//l:p节点的左子节点 pp:p节点的爸爸节点 lr:p节点的左子节点的右子节点
TreeNode l, pp, lr;
//旋转节点p非空并且p节点的左子节点非空
if (p != null && (l = p.left) != null) {
//将p节点的左子节点设置为左子节点的右子节点
if ((lr = p.left = l.right) != null)
//然后将p节点的左子节点的右子节点的父节点设置为p
lr.parent = p;
//将p节点的左子节点的父节点设置为p的父节点,如果为空的话,说明l就是根节点了
if ((pp = l.parent = p.parent) == null)
//染色成黑
(root = l).red = false;
//如果p节点是pp节点的右子节点的话,将爸爸节点pp的右子节点设置为l
else if (pp.right == p)
pp.right = l;
//如果p节点是pp节点的左子节点的话,将爸爸节点pp的左子节点设置为l
else
pp.left = l;
//最后将l节点的右子节点设置为p
l.right = p;
//将p节点的父节点设置为l
p.parent = l;
}
return root;
}
基本方法都介绍了,还是挺复杂。