JDK1.8 HashMap原理及源码分析

概述

JDK1.8对HashMap底层的实现进行了优化,引入红黑树的数据结构和扩容的优化等。

 

HashMap源码分析


HashMap的put方法执行过程:

putVal()源码分析:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
	Node<K, V>[] tab;
	Node<K, V> p;
	int n, i;
	if ((tab = table) == null || (n = tab.length) == 0)
        // 数组为空则初始化
		n = (tab = resize()).length;
	if ((p = tab[i = (n - 1) & hash]) == null)
        // 数组桶为空则作为头节点插入
		tab[i] = newNode(hash, key, value, null);
	else {
		Node<K, V> e;
		K k;
		if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
			e = p;
		else if (p instanceof TreeNode)
			// 如果当前的bucket里面已经是红黑树的话,执行红黑树的添加操作
			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);
					// TREEIFY_THRESHOLD = 8,判断如果当前bucket的位置链表长度大于8的话就将此链表变成红黑树。
					if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
						treeifyBin(tab, hash);
					break;
				}
				if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
					break;
				p = e;
			}
		}
		if (e != null) { // existing mapping for key
			V oldValue = e.value;
			if (!onlyIfAbsent || oldValue == null)
				e.value = value;
			afterNodeAccess(e);
			return oldValue;
		}
	}
	++modCount;
	if (++size > threshold)
		resize();
	afterNodeInsertion(evict);
	return null;
}

 通过hash计算项应该插入的桶位:
  如果存在相同key值则根据设置参数是否执行覆盖;
  如果相应桶位为空直接插入,否则判断桶位节点是红黑树结构还是链表结构;
  1、红黑树则调用putTreeVal() 添加红黑树节点方法
  2、链表则顺序遍历链表查找,如果存在相同key值节点则根据参数onlyIfAbsent选择是否覆盖原值如果不存在则在链表最后面插入节点。
  注:链表长度大于TREEIFY_THRESHOLD(默认8)且数组长度大于MIN_TREEIFY_CAPACITY(默认64)则调用treeifyBin() 链表树化方法对其进行树化。

红黑树(red black tree)

  • 节点是红色或黑色。
  • 根节点是黑色。
  • 每个叶节点(NIL节点,空节点)是黑色的。
  • 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
  • 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。、

注: 1、 将一个新项插入到RB Tree中,为了维持树的高度必须在插入一个新的项后在树的结构上进行改变主要通过旋转
    2、由于被插入前的树结构是构建好的,如果新添加的节点为黑色会破坏原有路径上的黑色节点的数量平等关系,所以默认新增节点为红色。
    3、因为如果子节点是红色在添加的时候只能添加黑色的节点,然而添加黑色叶子节点都会破坏树的第四条性质,所以要对其进行颜色变换
    4、
默认添加的叶子节点是红色的,所以添加到黑色节点后并不会破坏树的第四条结构

两种旋转和一种典型的变换

 左旋转                                                                                     右旋转

                      

双向旋转

 

注:维护红黑树结构的方法全部都在TreeNode类内部。

putTreeVal() 红黑树添加节点方法

     HashMap 中往红黑树中添加一个新节点 n 时,有以下操作:
     从根节点开始遍历当前红黑树中的元素 p,对比 n 和 p 的哈希值;
     如果哈希值相等则直接返回该节点;
     如果哈希值相等且键也相等,但是两个节点的键不是一个类 则对比子树;
     如果哈希值相等但键无法比较,则通过其他方法tieBreakOrder()对比,如通过对象引用内存地址得到哈希值比较结果
     如果当前节点 p 还没有左孩子或者右孩子时才能插入,否则就进入下一轮循环;
     插入元素后还需要进行红黑树平衡调整balanceInsertion(),并且要调用moveRootToFront()确保根节点为桶的第一个元素。

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;
	TreeNode<K, V> root = (parent != null) ? root() : this;
    // 每次添加元素时,从根节点遍历,对比哈希值
	for (TreeNode<K, V> p = root;;) {
		int dir, ph;
		K pk;
		if ((ph = p.hash) > h)
			dir = -1;
		else if (ph < h)
			dir = 1;
		else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            // 如果当前节点key与新增的一致直接返回当前节点
			return p;
		else if ((kc == null && (kc = comparableClassFor(k)) == null)
				|| (dir = compareComparables(kc, k, pk)) == 0) {
            //如果当前节点和要添加的节点哈希值相等,但是两个节点的键不是一个类,去对比左右孩子
			if (!searched) {
				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))
                    //如果从 ch 所在子树中可以找到要添加的节点,就直接返回
					return q;
			}
            //哈希值相等,但键无法比较,只好通过特殊的方法给个结果
			dir = tieBreakOrder(k, pk);
		}
        // 得到了要插入的节点与当前节点的大小关系:小就插到左子树,大就插到右子树
		TreeNode<K, V> xp = p;
        // 如果当前节点还没有左孩子或者右孩子时才能插入,否则就进入下一轮循环 
		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;
            //红黑树中,插入元素后必要的平衡调整操作
			moveRootToFront(tab, balanceInsertion(root, x));
			return null;
		}
	}
}

tieBreakOrder() 通过对象引用内存地址得到哈希值比较结果

/**
 * Tie-breaking utility for ordering insertions when equal
 * hashCodes and non-comparable. We don't require a total
 * order, just a consistent insertion rule to maintain
 * equivalence across rebalancings. Tie-breaking further than
 * necessary simplifies testing a bit.
 */
//这个方法用于 a 和 b 哈希值相同但是无法比较时,直接根据两个引用的地址进行比较
//这个树里不要求完全有序,只要插入时使用相同的规则保持平衡即可
static int tieBreakOrder(Object a, Object b) {
	int d;
	if (a == null || b == null ||
		(d = a.getClass().getName().
		 compareTo(b.getClass().getName())) == 0)
        //System.identityHashCode方法是java根据对象在内存中的地址算出来的一个数值
		d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
			 -1 : 1);
	return d;
}

moveRootToFront() 红黑树根节点转化为桶的头节点

/**
 * Ensures that the given root is the first node of its bin.
 * 把红黑树的根节点设为 其所在的数组桶 的第一个元素
 * TreeNode既是一个红黑树结构,也是一个双链表结构
 * 保证树的根节点一定也要成为链表的首节点
 */
static <K, V> void moveRootToFront(Node<K, V>[] tab, TreeNode<K, V> root) {
	int n;
	if (root != null && tab != null && (n = tab.length) > 0) { // 根节点不为空 并且 HashMap的元素数组不为空
		int index = (n - 1) & root.hash; // 根据根节点的Hash值 和 HashMap的元素数组长度 取得根节点在数组中的位置
		TreeNode<K, V> first = (TreeNode<K, V>) tab[index]; // 首先取得该位置上的第一个节点对象
		if (root != first) { // 如果root节点与该节点 不同
			Node<K, V> rn; // 定义根节点的后一个节点
			tab[index] = root; // 把元素数组index位置的元素替换为root节点对象
			TreeNode<K, V> rp = root.prev; // 获取root节点对象的前一个节点
			if ((rn = root.next) != null) // 如果root节点的后节点不为空
				((TreeNode<K, V>) rn).prev = rp; // root后节点的前节点 指向到 root的前节点,相当于把root从链表中摘除
			if (rp != null) // 如果root的前节点不为空
				rp.next = rn; // root前节点的后节点 指向到 root的后节点
			if (first != null) // 如果数组第一个节点对象不为空
				first.prev = root; // 这个原有的元素的 前节点 指向到 root,相当于root目前位于链表的首位
			root.next = first; // 原来的第一个节点现在作为root的下一个节点,变成了第二个节点
			root.prev = null; // 首节点没有前节点
		}

		/*
		 * 这一步是防御性的编程 校验TreeNode对象是否满足红黑树和双链表的特性
		 * 如果这个方法校验不通过:可能是因为用户编程失误,破坏了结构(例如:并发场景下);也可能是TreeNode的实现有问题(这个是理论上的以防万一);
		 */
		assert checkInvariants(root);
	}
}

 

treeifyBin() 链表树化方法

final void treeifyBin(Node<K, V>[] tab, int hash) {
	int n, index;
	Node<K, V> e;
	if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        // 扩容
		resize();
	// 通过hash计算头节点的位置。
	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 {
				// 将所有TreeNode连接在一起此时只是链表结构。
				p.prev = tl;
				tl.next = p;
			}
			tl = p;
		} while ((e = e.next) != null);

        //以链表的方式连接在一起,然后通过它构建红黑树
		if ((tab[index] = hd) != null)
			// 对TreeNode链表进行树化。
			hd.treeify(tab);
	}
}

treeify() 红黑树转化方法

final void treeify(Node<K, V>[] tab) {
	TreeNode<K, V> root = null;
	// 以for循环的方式遍历刚才我们创建的链表。
	for (TreeNode<K, V> x = this, next; x != null; x = next) {
		// next向前推进。
		next = (TreeNode<K, V>) x.next;
		x.left = x.right = null;
		// 为树根节点赋值。
		if (root == null) {
			x.parent = null;
			x.red = false;
			root = x;
		} else {
			// x即为当前访问链表中的项。
			K k = x.key;
			int h = x.hash;
			Class<?> kc = null;
			// 此时红黑树已经有了根节点,上面获取了当前加入红黑树的项的key和hash值进入核心循环。
			// 这里从root开始,是以一个自顶向下的方式遍历添加。
			// for循环没有控制条件,由代码内break跳出循环。
			for (TreeNode<K, V> p = root;;) {
				// dir:directory,比较添加项与当前树中访问节点的hash值判断加入项的路径,-1为左子树,+1为右子树。
				// ph:parent hash。
				int dir, ph;
				K pk = p.key;
				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)
					dir = tieBreakOrder(k, pk);

				// xp:x parent。
				TreeNode<K, V> xp = p;
				// 找到符合x添加条件的节点。
				if ((p = (dir <= 0) ? p.left : p.right) == null) {
					x.parent = xp;
					// 如果xp的hash值大于x的hash值,将x添加在xp的左边。
					if (dir <= 0)
						xp.left = x;
					// 反之添加在xp的右边。
					else
						xp.right = x;
					// 维护添加后红黑树的红黑结构。
					root = balanceInsertion(root, x);

					// 跳出循环当前链表中的项成功的添加到了红黑树中。
					break;
				}
			}
		}
	}
	moveRootToFront(tab, root);
}

     第一次循环会将链表中的首节点作为红黑树的根,而后的循环会将链表中的的项通过比较hash值连接到相应树节点的左边(小)或者右边(大)。
     注:插入可能会破坏树的结构所以要执行balanceInsertion()平衡红黑树结构。

balanceInsertion()平衡红黑树方法

static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root, TreeNode<K, V> x) {
	// 新加入树节点默认都是红色的,不会破坏树的结构。
	x.red = true;

	// xp:x parent,代表x的父节点。
	// xpp:x parent parent,代表x的祖父节点
	// xppl:x parent parent left,代表x的祖父的左节点。
	// xppr:x parent parent right,代表x的祖父的右节点。
	for (TreeNode<K, V> xp, xpp, xppl, xppr;;) {
		// 如果x的父节点为null说明只有一个节点,该节点为根节点,根节点为黑色,red = false。
		if ((xp = x.parent) == null) {
			x.red = false;
			return x;
		}
		// 进入else说明不是根节点。
		// 如果父节点是黑色,那么大吉大利(今晚吃鸡),红色的x节点可以直接添加到黑色节点后面,返回根就行了不需要任何多余的操作。
		// 如果父节点是红色的,但祖父节点为空的话也可以直接返回根此时父节点就是根节点,因为根必须是黑色的,添加在后面没有任何问题。
		else if (!xp.red || (xpp = xp.parent) == null)
			return root;

		// 一旦我们进入到这里就说明了两件是情
		// 1.x的父节点xp是红色的,这样就遇到两个红色节点相连的问题,所以必须经过旋转变换。
		// 2.x的祖父节点xpp不为空。

		// 判断如果父节点是否是祖父节点的左节点
		if (xp == (xppl = xpp.left)) {
			// 父节点xp是祖父的左节点xppr
			// 判断祖父节点的右节点不为空并且是否是红色的
			// 此时xpp的左右节点都是红的,所以直接进行上面所说的第三种变换,将两个子节点变成黑色,将xpp变成红色,然后将红色节点x顺利的添加到了xp的后面。
			// 这里大家有疑问为什么将x = xpp?
			// 这是由于将xpp变成红色以后可能与xpp的父节点发生两个相连红色节点的冲突,这就又构成了第二种旋转变换,所以必须从底向上的进行变换,直到根。
			// 所以令x = xpp,然后进行下下一层循环,接着往上走。
			if ((xppr = xpp.right) != null && xppr.red) {
				xppr.red = false;
				xp.red = false;
				xpp.red = true;
				x = xpp;
			}
			// 进入到这个else里面说明。
			// 父节点xp是祖父的左节点xppr。
			// 祖父节点xpp的右节点xppr是黑色节点或者为空,默认规定空节点也是黑色的。
			// 下面要判断x是xp的左节点还是右节点。
			else {
				// x是xp的右节点,此时的结构是:xpp左->xp右->x。这明显是第二中变换需要进行两次旋转,这里先进行一次旋转。
				// 下面是第一次旋转。
				if (x == xp.right) {
					root = rotateLeft(root, x = xp);
					xpp = (xp = x.parent) == null ? null : xp.parent;
				}
				// 针对本身就是xpp左->xp左->x的结构或者由于上面的旋转造成的这种结构进行一次旋转。
				if (xp != null) {
					xp.red = false;
					if (xpp != null) {
						xpp.red = true;
						root = rotateRight(root, xpp);
					}
				}
			}
		}
		// 这里的分析方式和前面的相对称只不过全部在右测不再重复分析。
		else {
			if (xppl != null && xppl.red) {
				xppl.red = false;
				xp.red = false;
				xpp.red = true;
				x = xpp;
			} else {
				if (x == xp.left) {
					root = rotateRight(root, x = xp);
					xpp = (xp = x.parent) == null ? null : xp.parent;
				}
				if (xp != null) {
					xp.red = false;
					if (xpp != null) {
						xpp.red = true;
						root = rotateLeft(root, xpp);
					}
				}
			}
		}
	}
}

下面简述前面的两种幸运的情况

  1. x本身为根节点返回x。
  2. x的父节点为黑色或者x的父节点是根节点直接返回不需要变换。

      如若上述两个条件不满足的话,就要进行变换了。

颜色变换

if (xp == (xppl = xpp.left)) {
	if ((xppr = xpp.right) != null && xppr.red) {
		xppr.red = false;
		xp.red = false;
		xpp.red = true;
		x = xpp;
	}
}

这里是一个典型的一个黑色节点的两个子节点都是红色的所以要进行颜色变换。

变换后x可以直接插入在xp的后面。

两个核心旋转(左旋转和右旋转)

我们已知xp是xpp的左节点,首先判断了x是xp的左节点还是右节点,如果是右节点的话构成了下面的结构。

这中结构需要进行双旋转,首先先进行一次向左旋转。

rotateLeft()左旋转

static <K, V> TreeNode<K, V> rotateLeft(TreeNode<K, V> root, TreeNode<K, V> p) {
	// r:right,右节点。
	// pp:parent parent,父节点的父节点。
	// rl:right left,右节点的左节点。
	TreeNode<K, V> r, pp, rl;
	if (p != null && (r = p.right) != null) {
		if ((rl = p.right = r.left) != null)
			rl.parent = p;
		if ((pp = r.parent = p.parent) == null)
			(root = r).red = false;
		else if (pp.left == p)
			pp.left = r;
		else
			pp.right = r;
		r.left = p;
		p.parent = r;
	}
	return root;
}

1.刚进入方法时,状态如下图。(RL可能是空的)

2.进入了if块后执行到第10行后。

 9             if ((rl = p.right = r.left) != null)
10                 rl.parent = p;

此时如果9行的条件符合的话执行10行RL指向P,如果RL为null的话,P的右节点指向null。

3.接着看11和12行代码。

11             if ((pp = r.parent = p.parent) == null)
12                 (root = r).red = false;

首先我们看11行if里面的赋值语句所造成的影响。

在if里面的表达式不管符不符合条件()内的内容都会执行。

如果符合条件的话会执行12行的代码,变成了下面的结果。

由于PP为空所以只剩下这三个。

4.如果11行的条件为假的话,执行完11行()内的表达式后执行13行

13             else if (pp.left == p)
14                 pp.left = r;

 

满足条件的话R和PP互相关联。

5.由于进入了13和14行所以不进入15和16行的else语句。

15             else
16                 pp.right = r;

6.看17和18行。

17             r.left = p;
18             p.parent = r;

最终执行完了一个旋转变成了我们开始说的第一种旋转的形式,这个结构还需要向右旋转一次。

    if (x == xp.right) {
       root = rotateLeft(root, x = xp);
       xpp = (xp = x.parent) == null ? null : xp.parent;
    }
    
    xpp = (xp = x.parent) == null ? null : xp.parent;

执行完上面的代码,旋转后调整x,xp,和xpp的关系得到下图。

rotateRight() 右旋转

if (xp != null) {
    xp.red = false;
    if (xpp != null) {
       xpp.red = true;
       root = rotateRight(root, xpp);
    }
}

1.首先让XP变成黑色。

 

2.如果XPP不为空的话变成红色。

由于我们在rotateLeft(root, xpp),传进来的是XXP所以下面的的旋转中实际上就是对XP和XXP执行了一次与上面的方向相反其他完全相同的旋转。

右旋转源码

static <K, V> TreeNode<K, V> rotateRight(TreeNode<K, V> root, TreeNode<K, V> p) {
	// l:left,左节点。
	// pp:parent parent,父节点的父节点。
	// lr:left right,左节点的右节点。
	TreeNode<K, V> l, pp, lr;
	if (p != null && (l = p.left) != null) {
		if ((lr = p.left = l.right) != null)
			lr.parent = p;
		if ((pp = l.parent = p.parent) == null)
			(root = l).red = false;
		else if (pp.right == p)
			pp.right = l;
		else
			pp.left = l;
		l.right = p;
		p.parent = l;
	}
	return root;
}

3.刚进来的时候结构是这个样子。

在这里的P就是刚才传进来的XPP。

4.这里我们认为LR是存在的,其实这个不影响主要的旋转,为空就指向null呗,直接执行完9和10行。

 9  if ((lr = p.left = l.right) != null)
10     lr.parent = p;

5.在这里我们假使PP是存在的,直接执行完11的表达式不再执行12行。(不再分析不存在的情况)。

11   if ((pp = l.parent = p.parent) == null)
12      (root = l).red = false;

6.由于11行的条件不符合,现在直接执行13行的表达式,不符合执行15行else,执行16行。

15   else
16     pp.left = l;

7.最后执行层17和18行。

17   l.right = p;
18   p.parent = l;

 

最终完成两次的旋转。

疑问?

大家可能觉得和刚才接不上其实是这样的,刚才在右旋转前的时候的图像是这个样的。

因为我们传进来的是XPP,所以结合上一次的向左旋转我们在向右旋转的时候看到全图应该是这个样子的。(注:XPPP可能是XPP的左父节点也可能是右父节点这里不影响,而且可以是任意颜色)

现在知道为什么XPPP可以是任意颜色的了吧,因为旋转过后X是黑色的即便XPPP是红色,此时我们又可以对两个红色的子节点进行颜色变换了,变换后X和XPPP有发生了颜色冲突,接着进行旋转直到根。

   static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root, TreeNode<K, V> x)
    {
        x.red = true;
        for (TreeNode<K, V> xp, xpp, xppl, xppr;;)
        {
            if ((xp = x.parent) == null)
            {
                x.red = false;
                return x;
            }
            else if (!xp.red || (xpp = xp.parent) == null)
                return root;
            if (xp == (xppl = xpp.left))
            {
               // 插入位置父节点在祖父节点的左边。
            }
            else
            {
         // 插入位置父节点在祖父节点的右边。
            }
        }
    }

getTreeNode() 红黑树中查找元素

HashMap 的查找是 get()方法

public V get(Object key) {
	Node<K,V> e;
	return (e = getNode(hash(key), key)) == null ? null : e.value;
}

它通过计算指定 key 的哈希值后,调用内部 getNode()方法

/**
 * Implements Map.get and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @return the node, or null if none
 */
final Node<K, V> getNode(int hash, Object key) {
	Node<K, V>[] tab;
	Node<K, V> first, e;
	int n;
	K k;
	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;
}

getNode() 方法根据哈希表元素个数与哈希值求模( (n - 1) &hash)得到 key 所在的桶的头结点,如果头节点是红黑树节点,就调用红黑树节点的 getTreeNode() 方法,否则就遍历链表节点。

/**
 * Calls find for root node.
 */
final TreeNode<K, V> getTreeNode(int h, Object k) {
	return ((parent != null) ? root() : this).find(h, k, null);
}

getTreeNode 方法使通过调用树形节点的 find() 方法进行查找

/**
 * Finds the node starting at root p with the given hash and key. The kc
 * argument caches comparableClassFor(key) upon first use comparing keys.
 */
final TreeNode<K, V> find(int h, Object k, Class<?> kc) {
	TreeNode<K, V> p = this;
	do {
		int ph, dir;
		K pk;
		TreeNode<K, V> pl = p.left, pr = p.right, q;
		if ((ph = p.hash) > h)
			p = pl;
		else if (ph < h)
			p = pr;
		// 对比节点的哈希值和要查找的哈希值相等,就会判断 key 是否相等,相等就直接返回;
		else if ((pk = p.key) == k || (k != null && k.equals(pk)))
			return p;
		else if (pl == null)
			p = pr;
		else if (pr == null)
			p = pl;
		else if ((kc != null || (kc = comparableClassFor(k)) != null) && (dir = compareComparables(kc, k, pk)) != 0)
			p = (dir < 0) ? pl : pr;
		// 从子树中递归查找。
		else if ((q = pr.find(h, k, kc)) != null)
			return q;
		else
			p = pl;
	} while (p != null);
	return null;
}

remove()删除节点

        remove方法底层实际上是调用了removeNode()方法来删除键值对节点,并且根据返回的节点对象取得key对应的值

public V remove(Object key) {
	Node<K,V> e;
	return (e = removeNode(hash(key), key, null, false, true)) == null ?
		null : e.value;
}

removeNode()方法

/**
* 方法为final,不可被覆写,子类可以通过实现afterNodeRemoval方法来增加自己的处理逻辑(解析中有描述)
*
* @param hash key的hash值,该值是通过hash(key)获取到的
* @param key 要删除的键值对的key
* @param value 要删除的键值对的value,该值是否作为删除的条件取决于matchValue是否为true
* @param matchValue 如果为true,则当key对应的键值对的值equals(value)为true时才删除;否则不关心value的值
* @param movable 删除后是否移动节点,如果为false,则不移动
* @return 返回被删除的节点对象,如果没有删除任何节点则返回null
*/
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; // 声明节点数组、当前节点、数组长度、索引值
    /*
     * 如果 节点数组tab不为空、数组长度n大于0、根据hash定位到的节点对象p(该节点为 树的根节点 或 链表的首节点)不为空
     * 需要从该节点p向下遍历,找到那个和key匹配的节点对象
     */
    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; // 定义要返回的节点对象,声明一个临时节点变量、键变量、值变量
 
        // 如果当前节点的键和key相等,那么当前节点就是要删除的节点,赋值给node
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
 
        /*
         * 到这一步说明首节点没有匹配上,那么检查下是否有next节点
         * 如果没有next节点,就说明该节点所在位置上没有发生hash碰撞, 就一个节点并且还没匹配上,也就没得删了,最终也就返回null了
         * 如果存在next节点,就说明该数组位置上发生了hash碰撞,此时可能存在一个链表,也可能是一颗红黑树
         */
        else if ((e = p.next) != null) {
            // 如果当前节点是TreeNode类型,说明已经是一个红黑树,那么调用getTreeNode方法从树结构中查找满足条件的节点
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            // 如果不是树节点,那么就是一个链表,只需要从头到尾逐个节点比对即可    
            else {
                do {
                    // 如果e节点的键是否和key相等,e节点就是要删除的节点,赋值给node变量,调出循环
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                            (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
 
                    // 走到这里,说明e也没有匹配上
                    p = e; // 把当前节点p指向e,这一步是让p存储的永远下一次循环里e的父节点,如果下一次e匹配上了,那么p就是node的父节点
                } while ((e = e.next) != null); // 如果e存在下一个节点,那么继续去匹配下一个节点。直到匹配到某个节点跳出 或者 遍历完链表所有节点
            }
        }
 
        /*
         * 如果node不为空,说明根据key匹配到了要删除的节点
         * 如果不需要对比value值  或者  需要对比value值但是value值也相等
         * 那么就可以删除该node节点了
         */
        if (node != null && (!matchValue || (v = node.value) == value ||
                                (value != null && value.equals(v)))) {
            if (node instanceof TreeNode) // 如果该节点是个TreeNode对象,说明此节点存在于红黑树结构中,调用removeTreeNode方法(该方法单独解析)移除该节点
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p) // 如果该节点不是TreeNode对象,node == p 的意思是该node节点就是首节点
                tab[index] = node.next; // 由于删除的是首节点,那么直接将节点数组对应位置指向到第二个节点即可
            else // 如果node节点不是首节点,此时p是node的父节点,由于要删除node,所有只需要把p的下一个节点指向到node的下一个节点即可把node从链表中删除了
                p.next = node.next;
            ++modCount; // HashMap的修改次数递增
            --size; // HashMap的元素个数递减
            afterNodeRemoval(node); // 调用afterNodeRemoval方法,该方法HashMap没有任何实现逻辑,目的是为了让子类根据需要自行覆写
            return node;
        }
    }
    return null;
}

removeTreeNode图解

 

resize() 扩容方法

          resize首先完成了新存储数组的capacity和threshold的计算:每一次扩容,newCapacity和newThreshold均是扩容前值的两倍,然后将原数据迁移到新存储结构中:
         遍历原数组桶,原桶中头节点非空
         判定头节点是否存在下一个节点,若不存在则计算新的存储位置hash & (newCap - 1)绑定到新桶;
         判定节点为红黑树 调用split()
         判定节点为链表 遍历链表计算节点是否需要变动 (e.hash & oldCap) == 0 保持不变 否则节点存储到新桶的newTab[j + oldCap] 位置

注:bucket节点下标计算公式:index = hash & (table.length - 1)

扩容机制JDK1.8具体说明:

使用的是2次幂的扩展(指长度扩为原来2倍),扩容后元素的位置要么在原位置,要么在原位置再移动2次幂的位置。如下图 n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。

元素在重新计算hash之后,n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化: 

因此,JDK1.8扩充HashMap的时候,不像JDK1.7的实现重新计算hash,只判定原来的hash值新增的那个bit是1还是0,是0索引没变,是1索引变成“原索引+oldCap”。这样省去了重新计算hash值的时间,而且新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucketz。

下图为16扩充为32的resize示意图:

扩容源码分析:

/**
 * Initializes or doubles table size. If null, allocates in accord with initial
 * capacity target held in field threshold. Otherwise, because we are using
 * power-of-two expansion, the elements from each bin must either stay at same
 * index, or move with a power of two offset in the new table.
 *
 * @return the table
 */
// 扩容兼初始化
final Node<K, V>[] resize() {
	Node<K, V>[] 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 接近MAXIMUM_CAPACITY的两倍
			threshold = Integer.MAX_VALUE;
			return oldTab;
		} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {
			// 新数组长度和临界值 扩大为原来的2倍
			newThr = oldThr << 1;
		}
	} else if (oldThr > 0) // initial capacity was placed in threshold
		// 如果原数组的thredshold大于0则将容量设为原来的thredshold(在第一次带参数初始化时出现)
		newCap = oldThr;
	else { // zero initial threshold signifies using defaults
			// 默认无参数初始化
		newCap = DEFAULT_INITIAL_CAPACITY;// 16
		newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);// 0.75*16=12
	}
	if (newThr == 0) {
		// 如果新 的容量 ==0
		float ft = (float) newCap * loadFactor;// loadFactor 加载因子 默认0.75,可在初始化时传入,16*0.75=12 可以放12个键值对
		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;
	// 如果原来的table有数据,则将数据迁移到新的table中
	if (oldTab != null) {
		// 遍历原数组,将非空元素进行复制
		for (int j = 0; j < oldCap; ++j) {
			Node<K, V> e;
			if ((e = oldTab[j]) != null) {
				oldTab[j] = null;
				// 如果链表只有一个,则进行直接赋值
				if (e.next == null)
					// e.hash & (newCap - 1) 确定元素存放位置
					newTab[e.hash & (newCap - 1)] = e;
				else if (e instanceof TreeNode)
					// 红黑树数据迁移
					((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
				else { // preserve order
						// 链表复制:采用了 原始位置加原数组长度的方法定位新的存储位置
					Node<K, V> loHead = null, loTail = null;
					Node<K, V> hiHead = null, hiTail = null;
					Node<K, V> next;
					do {
						next = e.next;
						// (e.hash & oldCap) 得到的是 元素的在数组中的位置是否需要移动
						if ((e.hash & oldCap) == 0) {
							// 0 原元素索引位置不变
							if (loTail == null)
								loHead = e;// 确定头节点
							else
								loTail.next = e;// 将节点索引位置不变的连接到头节点后
							loTail = e;
						} else {
							// 1 原元素索引位置变化“原索引+oldCap”
							if (hiTail == null)
								hiHead = e;
							else
								hiTail.next = e;
							hiTail = e;
						}
					} while ((e = next) != null);
					// 总结:1.8中 旧链表迁移新链表 链表元素相对位置没有变化; 实际是对对象的内存地址进行操作
					// 在1.7中 旧链表迁移新链表 如果在新表的数组索引位置相同,则链表元素会倒置
					if (loTail != null) {
						loTail.next = null;// 将链表的尾节点 的next 设置为空
						newTab[j] = loHead;
					}
					if (hiTail != null) {
						hiTail.next = null;// 将链表的尾节点 的next 设置为空
						newTab[j + oldCap] = hiHead;
					}
				}
			}
		}
	}
	return newTab;
}
}

树形结构修剪 split()

当扩容时,如果当前桶中元素结构是红黑树,进行数据迁移修剪主要分两部分:
1.分类 根据(hash & bit) == 0 分类,true放到原索引位置,false放到“原索引+oldCap”位置。
2.根据元素个数决定还原链表还是保留红黑是结构 元素个数小于链表还原阈值 UNTREEIFY_THRESHOLD (默认为 6)时还原成链表,
否则重新构建红黑树只是移除了不符合要求的元素(即(hash & bit) == 0 为false的元素)。

源码:

/**
 * Splits nodes in a tree bin into lower and upper tree bins, or untreeifies if
 * now too small. Called only from resize; see above discussion about split bits
 * and indices.
 *
 * @param map the map
 * @param tab the table for recording bin heads
 * @param index the index of the table being split
 * @param bit the bit of hash to split on
 */
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;
		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;
		}
	}

	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);
		}
	}
}

/**
 * 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) {
		Node<K, V> p = map.replacementNode(q, null);
		if (tl == null)
			hd = p;
		else
			tl.next = p;
		tl = p;
	}
	return hd;
}

扩展:

 1、由于理想情况下使用随机的哈希码,容器中节点分布在hash桶中的频率遵循泊松分布,按照泊松分布的计算公式计算出了桶中元素个数和概率的对照表,可以看到链表中元素个数为8时的概率已经非常小,再多的就更少了。并且红黑树的平均查找长度是log(n) 长度为8时平均查找长度为3,如使用链表平均查找长度为8/2=4,所以才有选择链表元素个数8时转换为树的必要。
2、链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
3、数组长度保持2的次幂,length-1的低位都为1,扩容后只有最左位的1一位差异,这样在通过 h&(length-1)的时候,只要h对应的最左边的那一个差异位为0,就能保证得到的新的数组索引和老数组索引一致。并且&运算,高位是不会对结果产生影响的会使得获得的数组索引index更加均匀。

 

鸣谢:

https://www.cnblogs.com/finite/p/8251587.html

https://blog.csdn.net/u011240877/article/details/53358305

https://blog.csdn.net/weixin_42340670/article/details/80555860

https://blog.csdn.net/ExcellentYuXiao/article/details/52344819

https://blog.csdn.net/u013494765/article/details/77837338

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值