HashMap中哈希碰撞大于8真的会将链表转为红黑树吗?

前言

点赞在看,养成习惯。

点赞收藏,人生辉煌。

点击关注【微信搜索公众号:编程背锅侠】,防止迷路。

HashMap系列文章

第一篇 HashMap源码中的成员变量你还不懂? 来来来!!!整理好的成员变量源码解析

第二篇 撸啊撸,再次撸HashMap源码,踩坑源码中构造方法!!!每次都有收获

第三篇 MoxiMoxi !!!你看过HashMap中的put方法的源码吗?

第四篇 HashMap源码中的resize扩容方法除了扩容还有一个用途你真的知道吗?

第五篇 HashMap中哈希碰撞大于8真的会将链表转为红黑树吗?

final void treeifyBin(HashMap.Node<K,V>[] tab, int hash)将当前桶下的链表中的Node节点类型转化为TreeNode节点类型,并转换为红黑树

节点添加完成之后判断此时节点个数是否大于TREEIFY_THRESHOLD临界值8,如果大于则将链表转换为红黑树,转换红黑树的方法 treeifyBin,整体代码如下:

if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 
  // 转换为红黑树 tab表示数组名 hash表示哈希值 
  treeifyBin(tab, hash);

真的是只要TREEIFY_THRESHOLD大于临界值8就转化为红黑树吗?

(n = tab.length) < MIN_TREEIFY_CAPACITY这句源码。MIN_TREEIFY_CAPACITY的值为64。其实转换为红黑色的条件是有两个。一个条件是大于临界值8,另一个条件就是容量要大于等于64。

为什么容量要大于64才允许树形化?

如果数组很⼩,转换为红黑树,遍历效率要低很多。如果又这个条件,会进行扩容,那么就会重新计算哈希值,链表长度有可能就变短了,数据会放到数组中,这样相对来说效率⾼一些。

源码阅读

// tab数组名
// 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)
		// 容量达不到64就去执行扩容方法
		resize();
	// 将数组中的元素取出赋值给e,e是哈希表中指定位置桶里的链表节点,从第一个开始,e:是头节点  
	else if ((e = tab[index = (n - 1) & hash]) != null) {
    // 执⾏到这⾥说明哈希表中的数组长度⼤于阈值64,开始进行树形化
		// hd红黑树的头节点,tl红黑树的尾节点
		TreeNode<K,V> hd = null, tl = null;
		// 以下这个循环就是将链表结构中的各个节点转换为数节点,也就是一串树节点,并没有转换为红黑树
		do {
			// 新创建一个树的节点,内容和当前链表节点e一致
			// 第一次的时候这个节点是一个根节点
      // 将Node节点转换为TreeNode节点
			TreeNode<K,V> p = replacementTreeNode(e, null);
			// 第一次循环的时候tl = null, 
			if (tl == null)
				// 将新创键的p节点赋值给红黑树的头结点
				hd = p;
			else {
				// p.prev = tl:将上一个节点p赋值给现在的p的前一个节点
				p.prev = tl;
				// tl.next = p;将现在节点p作为树的尾结点的下一个节点
				tl.next = p;
			}
			// 第一次循环的时候,将新创键的p节点赋值给红黑树的尾结点,此时,这个节点即是头节点也是尾节点
			tl = p;
			// e = e.next 将当前节点的下一个节点赋值给e,如果下一个节点,不等于null则回到上面继续取出链表中节点转换为红黑树    
		} while ((e = e.next) != null);
		// 让桶中的第一个元素即数组中的元素指向新建的红黑树的节点,以后这个桶里的元素就是红黑树而不是链表数据结构了
		// 将根节点放到了桶里面
		if ((tab[index] = hd) != null)
      // 转换为红黑树的真正的实现代码
			hd.treeify(tab);
	}
}

源码总结

  • 1.根据哈希表中元素个数确定是扩容还是树形化 。必须满足以下两个条件
  • 2.如果是树形化遍历桶中的元素,创建相同个数的树形节点,复制内容,建⽴起联系。
  • 3.然后让桶中的第⼀个元素指向新创建的树根节点,替换桶的链表内容为树形化内容。

转换为红黑树的源码分析

源码分析

// tab: 集合中的所有的Node节点,其实红黑树的第一个节点还是Node节点
final void treeify(Node<K,V>[] tab) {
	// 定义一个root节点
	TreeNode<K,V> root = null;
	// 遍历这个已经转换为树节点的链表,x指向当前节点、next指向下一个节点,首次遍历的时候这个节点就是根节点
	for (TreeNode<K,V> x = this, next; x != null; x = next) {
		// 将这个节点的下一个节点并强制转换为树节点
		next = (TreeNode<K,V>)x.next;
		// 初始化这个节点的左子树和右子树节点为null
		x.left = x.right = null;
		// 判断根节点是否为null,将当前的节点设置为根节点,也就是说有没有根节点
		// 第一次遍历,会进入这个判断,找出根节点
		if (root == null) {
			// 根节点的父节点设置为null
			x.parent = null;
			// 节点的颜色设置为黑
			x.red = false;
			// 将当前的这个节点赋值给根节点root,只有一个节点赋值成功,也就是说根节点指向当前节点
			root = x;
		}
		else {// 此时,已经存在根节点了
			// 获取当前节点的key赋值给k
			K k = x.key;
			// 获取当前节点的哈希值赋值给h
			int h = x.hash;
			// 定义key所属的Class
			Class<?> kc = null;
			// 真正的构建红黑树
			for (TreeNode<K,V> p = root;;) {
				// dir 标识方向,是在根节点的左侧还是右侧
				// ph标识当前树节点的hash值
				int dir, ph;
				// 当前根节点的key赋值给pk
				K pk = p.key;
				// 将根节点hash赋值给ph,如果当前根节点hash值大于当前链表节点的hash值
				if ((ph = p.hash) > h)
					// 标识当前链表节点会放到当前根节点的左侧
					dir = -1;
					// 将根节点hash赋值给ph,如果当前根节点hash值小于当前链表节点的hash值
				else if (ph < h)
					// 标识当前链表节点会放到当前根节点的右侧
					dir = 1;
					// 将根节点hash赋值给ph,如果当前根节点hash值等于当前链表节点的hash值
					// 如果当前链表节点的key实现了comparable接口,并且当前树节点和链表节点是相同Class的实例
					// 那么通过comparable的方式再比较两者。
					// 如果还是相等,最后再通过tieBreakOrder比较一次
					// dir = compareComparables(kc, k, pk)) == 0等于0代表还是平衡
				else if ((kc == null && (kc = comparableClassFor(k)) == null) ||
						(dir = compareComparables(kc, k, pk)) == 0)
					// 打破平衡
					dir = tieBreakOrder(k, pk);

				// 当前节点
				TreeNode<K,V> xp = p;
				// dir <= 0:当前链表节点放置在当前树节点的左侧,但不一定是该树节点的左子树,也可能是左子树的右子树或者更深层次的节点。
				// dir > 0:当前链表节点放置在当前树节点的右侧,但不一定是该树节点的右子树,也可能是右子树的左子树或者更深层次的节点。
				// 如果当前树节点不是叶子节点,那么最终会以当前树节点的左子树或者右子树为起始节点接着遍历,重新寻找自己(当前链表节点)的位置
				// 如果当前树节点就是叶子节点,那么根据dir的值,就可以把当前链表节点挂载到当前树节点的左或者右侧了。
				// 挂载之后,还需要重新把树进行平衡。平衡之后,就可以针对下一个链表节点进行处理了。
				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;
				}
			}
		}
	}
	// 链表节点都遍历完后,最终构造出来的树可能经历多次平衡操作,根节点目前到底是链表的哪一个节点是不确定的。
	// 要将红黑树的根节点移动至链表节点的第一个位置也就是 table[i]的位置。
	moveRootToFront(tab, root);
}

记录9个hashCode相同的字符串

案例演示

@Test
public void test_hash_map_hash() {
	ArrayList<String> list = new ArrayList<>();
	list.add("3Qj");
	list.add("2pj");
	list.add("2qK");
	list.add("2r,");
	list.add("3RK");
	list.add("3S,");
	list.add("42j");
	list.add("43K");
	list.add("44,");
	list.forEach(s -> System.out.println(s + "  ====  " + s.hashCode()));
}

打印结果

3Qj  ====  51628
2pj  ====  51628
2qK  ====  51628
2r,  ====  51628
3RK  ====  51628
3S,  ====  51628
42j  ====  51628
43K  ====  51628
44,  ====  51628

谢谢点赞

  • 创作不易, 非常欢迎大家的点赞、评论和关注(^_−)☆
  • 你的点赞、评论以及关注是对我最大的支持和鼓励,而你的支持和鼓励
  • 我继续创作高质量博客的动力 !!!

猜你喜欢

转载自blog.csdn.net/wildwolf_001/article/details/107314470