深入分析 HashMap

Posted 努力的小鳴人

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入分析 HashMap相关的知识,希望对你有一定的参考价值。

❤写在前面
❤博客主页:努力的小鳴人
❤系列专栏:Java基础学习😋
❤欢迎小伙伴们,点赞👍关注🔎收藏🍔一起学习!
❤如有错误的地方,还请小伙伴们指正!🌹

强烈推荐【10章Java集合】几张脑图带你进入Java集合的头脑风暴

文章目录


一、JDK1.8前 HashMap 的缺点

  1. JDK1.8前HashMap的实现是数组+链表,即便哈希函数取得再好,也难以达到元素百分百均匀分布
  2. 当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候HashMap 就相当于一个单链表假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势
  3. 针对这种情况,JDK 1.8 中引入了红黑树(查找时间复杂度为 O(logn))来优化这个问题

二、JDK1.8中 HashMap 数据结构

HashMap 是数组+链表+红黑树实现的

🔥JDK1.8新增红黑树

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;

🔥红黑树中三个关键参数

👌TREEIFY_THRESHOLD

一个桶的树化阈值

  1. 例:static final int TREEIFY_THRESHOLD = 8
  2. 当桶中元素个数超过这个值时,需要使用红黑树节点替换链表节点

👌UNTREEIFY_THRESHOLD

一个树的链表还原阈值

  1. 例:static final int UNTREEIFY_THRESHOLD = 6
  2. 扩容时,桶中元素个数小于这个值就会把树形的桶元素还原为链表结构

👌MIN_TREEIFY_CAPACITY

哈希表的最小树形化容量

  1. 例:static final int MIN_TREEIFY_CAPACITY = 64
  2. 哈希表中容量大于这个值时,表中的桶才能进行树形化否则桶内元素太多时会扩容,而不是树形化,为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD

🔥JDK1.8中新增操作:桶的树形化

treeifyBin()

  1. 介绍:如果一个桶中的元素个数超过 TREEIFY_THRESHOLD(默认是 8 ),就使用红黑树来替换链表,从而提高速度,这个替换的方法叫 treeifyBin() 即树形化
  2. 以下步骤:
    1.根据哈希表中元素个数确定是扩容或者树形化
    2.如果是树形化遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系
    3.然后让桶第一个元素指向新建的树头结点,替换桶的链表内容为树形内容
 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();
	 else if ((e = tab[index = (n - 1) & hash]) != null) 
		 //如果哈希表中的元素个数超过了 树形化阈值,进行树形化
		 // e 是哈希表中指定位置桶里的链表节点,从第一个开始
	 TreeNode<K,V> hd = null, tl = null; //红黑树的头、尾节点
	 do 
		 //新建一个树形节点,内容和当前链表节点 e 一致
		 TreeNode<K,V> p = replacementTreeNode(e, null);
		 if (tl == null) //确定树头节点
			 hd = p;
		 else 
			 p.prev = tl;
			 tl.next = p;
		 
		 tl = p;
	  while ((e = e.next) != null);
	 //让桶的第一个元素指向新建的红黑树头结点,以后这个桶里的元素就是红黑树而不是链表了
	 if ((tab[index] = hd) != null)
		 hd.treeify(tab);
	 
 
	 TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) 
	 return new TreeNode<>(p.hash, p.key, p.value, next);
 

三、put 方法分析


图解:

  1. 判断键值对数组 table[i]是否为空或为 null,否则执行 resize()进行扩容
  2. 根据键值 key 计算 hash 值得到插入的数组索引 i,如果 table[i]==null,直接新建节点添加,转向6,如果 table[i]不为空,转向3
  3. 判断 table[i]的首个元素是否和 key 一样,如果相同直接覆盖 value,否则转向4,这里的相同指的是 hashCode 以及 equals
  4. 判断 table[i] 是否为 treeNode,即 table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向 5
  5. 遍历 table[i],判断链表长度是否大于 8,大于 8 的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现 key 已经存在直接覆盖 value
  6. 插入成功后,判断实际存在的键值对数量 size 是否超多了最大容量 threshold,如果超过,进行扩容

🔥put方法源码

源码加解析:

public V put(K key, V value) 
		// 对 key 的 hashCode()做 hash
		return putVal(hash(key), key, value, false, true);

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
			boolean evict) 
	Node<K,V>[] tab; Node<K,V> p; int n, i;
	// 步骤①:tab 为空则创建
	if ((tab = table) == null || (n = tab.length) == 0)
		n = (tab = resize()).length;
	// 步骤②:计算 index,并对 null 做处理
	if ((p = tab[i = (n - 1) & hash]) == null)
		tab[i] = newNode(hash, key, value, null);
	else 
		Node<K,V> e; K k;
		// 步骤③:节点 key 存在,直接覆盖 value
		if (p.hash == hash &&
			((k = p.key) == key || (key != null && key.equals(k))))
			e = p;
		// 步骤④:判断该链为红黑树
		else if (p instanceof TreeNode)
			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);
						//链表长度大于 8 转换为红黑树进行处理
					if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
						treeifyBin(tab, hash);
					break;
				
					// key 已经存在直接覆盖 value
				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;

🔥JDK1.8中新增操作:红黑树中查找元素

getTreeNode()
查找方法

  1. 通过计算指定 key 的哈希值后,调用内部方法 getNode()
  2. getNode() 方法是根据哈希表元素个数与哈希值求模(使用的公式是 (n - 1)&hash)得到 key 所在的桶的头结点,如果头节点恰好是红黑树节点,就调用红黑树节点的 getTreeNode() 方法,否则就遍历链表节点。
  3. getTreeNode 方法使通过调用树形节点的 find()方法进行查找
    final TreeNode<K,V> getTreeNode(int h, Object k)
    return ((parent != null) ? root() : this).find(h, k, null);
  4. 由于之前添加时已经保证这个树是有序的,因此查找时基本就是折半查找,效率很高
  5. 如果对比节点的哈希值和要查找的哈希值相等,就会判断 key 是否相等,相等就直接返回;不相等就从子树中递归查找
    源码:
	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;

四、扩容机制 JDK1.8 VS JDK1.7

假设了hash 算法是简单的用 key mod 一下表的大小(也就是数组的长度)其中的哈希桶数组 table 的 size=2, 所以 key = 3、7、5,put 顺序依次为 5、7、3,在 mod 2 以后都冲突在 table[1]这里了,这里假设负载因子 loadFactor=1,即当键值对的实际大小 size 大于 table 的实际大小时进行扩容。接下来的三个步骤是哈希桶数组 resize 成 4,然后所有的 Node 重新 rehash 的过程

🔥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 就会发生这样的变化:

因此,我们在扩充 HashMap 的时候,不需要像 JDK1.7 那样重新计算 hash,只需要看看原来的 hash 值新增的那个 bit 是 1 还是 0 就好了,是 0 的话索引没变,是 1 的话索引变成“原索引+oldCap”,可以看看下图为 16 扩充为 32 的 resize 示意图

既省去了重新计算 hash 值的时间,而且同时,由于新增的 1bit 是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了

🎁总结:写代码的时候必然会用到一些数据结构,其中尤为经典的就是HashMap,我还是多看看吧
👌 作者算是一名Java初学者,文章如有错误,欢迎评论私信指正,一起学习~~
😊如果文章对小伙伴们来说有用的话,点赞👍关注🔎收藏🍔就是我的最大动力!
🚩不积跬步,无以至千里书接下回,欢迎再见🌹

以上是关于深入分析 HashMap的主要内容,如果未能解决你的问题,请参考以下文章

深入分析 HashMap

Java 集合深入理解 :HashMap之实现原理及hash碰撞

HashMap红黑树原理及源码分析---图形注释一应俱全

HashMap红黑树原理及源码分析---图形注释一应俱全

聊聊并发——深入分析ConcurrentHashMap

底层原理之旅—HashMap深入浅出的源码分析(JDK1.8版本)