JDK8 HashMap 源码解析

JDK8以前HashMap的实现是数组加链表,JDK8以后实现是数组加链表加红黑树。

先说一下红黑树的几个必要条件

  • 节点分为红色或者黑色。
  • 根节点必为黑色。
  • 叶子节点都为黑色,且为 null。
  • 连接红色节点的两个子节点都为黑色(红黑树不会出现相邻的红色节点)。
  • 从任意节点出发,到其每个叶子节点的路径中包含相同数量的黑色节点。
  • 新加入到红黑树的节点为红色节点。

一、 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;
}

基本方法都介绍了,还是挺复杂。

你可能感兴趣的:(java,哈希算法,开发语言)