【昕宝爸爸小模块】JDK1.8中HashMap做了什么改变?

发布时间:2024年01月15日

在这里插入图片描述

??博客首页???????https://blog.csdn.net/Java_Yangxiaoyuan


???????欢迎优秀的你👍点赞、🗂?收藏、加??关注哦。


???????本文章CSDN首发,欢迎转载,要注明出处哦!


???????先感谢优秀的你能认真的看完本文,有问题欢迎评论区交流,都会认真回复!



一、?引言


Java 8是一个比较大的版本,目前很多人还在用,他有很多内容的升级,关于HashMap也有很多,其中最主要的就是引入了红黑树,除此之外 hashresize 等方法都有些改动。


Java Development Kit (JDK) 1.8(也被称为Java 8)中,HashMap 类有一些重要的改变和改进。以下是一些主要的改动:


  1. 默认的初始容量和加载因子:在JDK 1.8中,HashMap 的默认初始容量(initial capacity)从16增加到了16,默认加载因子(load factor)从0.75降低到了0.7。

  2. 使用Node类替代了Entry类:在JDK 1.8中,HashMap 的内部实现进行了重构,使用了一个新的内部类 Node<K,V> 来替代了原来的 Entry<K,V>Node 类包含了键(key)、值(value)和哈希值(hash)。

  3. 改进了哈希函数:JDK 1.8 对 HashMap 的哈希函数进行了改进,以提高其分布性和均匀性。这有助于减少哈希冲突,从而提高性能。

  4. 提供了更强大的迭代器:在JDK 1.8中,HashMap 的迭代器(iterator)现在支持更复杂的状态转换,比如删除元素。

  5. 更好的性能:通过改进哈希函数和内部实现,JDK 1.8中的 HashMap 在大多数情况下的性能都有所提高。

  6. 引入了ConcurrentHashMap:虽然 HashMap 在JDK 1.8中没有直接的变化,但引入了一个新的类 ConcurrentHashMap,它提供了线程安全的 HashMap 实现。

这些改变都是为了提高 HashMap 的性能和功能。不过,这也意味着在JDK 1.8中使用 HashMap 时需要注意一些新的特性和行为。


二、?拓展知识仓


2.1 🟢红黑树


Java 1.7中的HashMap使用一个数组+链表的数据结构来存储键值对,这会导致在处理hash冲突时性能下降,特别是当链表变得很长的时候时。Java 1.8中的HashMap引入了红黑树来替代链表,以解决中突时性能问题。这意味着当链表变得过长时,HashMap可以将链表转换为树,从而提高了查找、插入和删除操作的性能。


这里不做细讲了,前面博文已做详细说明:【Java集合篇】接上篇博文–为什么在JDK8中HashMap要转成红黑树


2.2 🟢节点变化


在JDK 1.8中,Node用于代替了旧版本中的普通链表节点(Entry) ,在旧版本中,每个桶中的元素都需要一个单独的Entry对象,而在新版本中,使用Node来代替,并且为了支持红黑树还引入了TreeNode。在Node中hash字段变成了final类型,一定确定就不再可变。


1.7中的Entry:


static class Entry<K,V> implements Map .Entry<K,V> {
	final K key;
	V value;
	Entry<KV> next;
	int hash;
}

1.8中的Node:


static class Node<K ,V> implements Map.Entry<K,V> {
	final int hash;
	final K key;
	V value;
	Node<K,V> next;
}

static final class Treelode<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;
}

2.3 🟢尾插法


为了解决1.7中的并发场景中存在的死循环的问题,JDK1.8把头插法改成了尾插法。


啊关于这个问题也不在这里做详细的解释,前面的博文也很详细的讲解了这个问题。请点击传送门前往此处!【昕宝爸爸小模块】HashMap用在并发场景存在的问题


2.4 🟢hash方法


在JDK 1.8中,hash方法有所变化。JDK 1.7中hash方法如下:


final int hash(Object k) {
	int h = 0;
	if (useAltHashing) {
		if (k instanceof string) {
			return sun.misc.Hashing.stringHash32((String) k);
		}

		h = hashSeed;
	}

	h ^= k.hashCode();
	

	// This function ensures that hashCodes that differ only by
	// constant multiples at each bit position have a bounded
	// number of collisions (approximately 8 at default load factor).
	h ^= (h >>> 20) ^ (h >>> 12);
	return h ^ (h >>> 7) ^ (h >>> 4);
}

而JDK 1.8中hash代码比较简单:


static final int hash(object key) {
	int h;
	return (key == null) ?  : (h = key.hashCode()) ^ (h >>> 16);
}

这两个算法比较大的区别就是1.8中的 位运算 异或 操作更少了,而1.7中之所以有这么多位运算和异或操作,主要是为了增加结果的散列性,从而减少碰撞的概率。


而JDK 1.8的源码中关于这个算法也给出了官方解释:


There is a tradeoff between speed, utility, and quality of bit-spreading. Because many
common sets of hashes are already reasonably distributed (so don’t benefit from
spreading), and because we use trees to handle large sets of collisions in bins, we just XOR
some shifted bits in the cheapest possible way to reduce systematic lossage, as well as to
incorporate impact of the highest bits that would otherwise never be used in index
calculations because of table bounds.


大概意思就是说,1.8的的哈希算法在速度、实用性和位扩散的质量之间做了权衡。修改哈希算法的目的是在保持速度的前提下,提高hashcode的质量,以减少哈希冲突和提高数据分布的均匀性。这里还是到在JDK 1.8 中,为了解决桶上的冲突,引入了红黑树。这意味着即使哈希冲突发生,树结构可以有效地处理大规模的冲突,而不会影响性能。


位扩散质量:位扩散是指在哈希算法中如何将不同位的信息组合,以生成最终的哈希码。较好的位扩散能够减少哈希冲突,即不同键映射到同一桶的可能性。因此,修改哈希算法的一个目标是提高位扩散的质量,以改善数据的分布。


2.5 🟢扩容机制


在JDK 1.8中,扩容机制也有很大的变化,在JDK 1.7中,扩容是通过 resize transfer 两个方法相互配合完成的。


void resize(int newCapacity) {
	Entry[] oldTable = table;
	int oldCapacity = oldTable.length;
	if (oldCapacity == MAXIMUM_CAPACITY) {
		threshold = Integer.MAX_VALUE;
		return;
	}
	
	Entry[] newTable = new Entry[newCapacity];
	boolean oldAltHashing = useAltHashing;
	useAltHashing = sun.misc.VM.isBooted() && (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
	boolean rehash = oldAltHashing ^ useAltHashing;
	transfer(newTable, rehash);
	table = newTable;
	threshold = (int)Math.min(newCapacity * loadFactor,MAXIMUM_CAPACITY + 1);
}

void transfer(Entryl] newTable, boolean rehash) {
	int newCapacity = newTable.length;
	for (Entry<K,V> e : table) {
		while(null != e) {
			Entry<K,V> next = e.next ;
			if (rehash) {
				e.hash = null == e.key ?  : hash(e.key);
			}
			int i = indexFor(e.hash, newCapacity);
			e.next = newTable[i];
			newTablelil = e;
			e = next;
		}
	}
}

而在JDK 1 . 8 中,把 transfer 的逻辑字节放到 resize 中了。


final Node<K,V>[] resize() {
	Node<K,V>[] oldTab = table;
	int oldCap = (oldTab == nul1) ?  : 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; // double threshold
		}
		// initial capacity was placed in threshold
		else if (oldThr > 0) {
			newCap = oldThr;
		}
		// zero initial threshold signifies using defaults
		else {
			newCap = DEFAULT_INITIAL_CAPACITY;
			newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
		}

		if (newThr == 0) {
			float ft = (float)newCap * loadFactor;
			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;
		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)
						newTab[e.hash & (newCap - 1)] = e;
					else if (e instanceof TreeNode)
						((TreeNode<K,V>)e).split(this,newTab,j,oldCap);
					// preserve order
					else {
						Node<K,V> loHead = null, loTail = null;
						Node<K,V> hiHead = null, hiTail = null;
						Node<K,V> next;
						
						do {
							next = e.next ;
							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;
							newTab[j + oldCap] = hiHead;
						}
					}
				}
			}
		}
		return newTab;
	} 
}


肉眼可见的代码变长了,主要是因为 1.8 中 引入了红黑树,需要考虑到树化的过程。


还有就是在JDK 1.8 中,元素扩容后的位置有所区别。


JDK1.7中扩容的时候,重新计算位置。JDK 1.8 则不一定,只要看看原hash值新增的那个bit位是1还是0,是0的话位置不变,是1的话位置重新计算,为原数组下标位置加上原数组容量:


do {
	next = e.next;
	// 结果为0,放入新数组的位置和原数组的位置相同
	if ((e.hash & oldCap) == 0) {
		if (loTail == null)
			loHead = e;
		else
			loTail.next = e;
		loTail = e;
	} 
	// 结果不为0,需要重新计算存放的位置
	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;
	newTab[j + oldCap] = hiHead;
}
文章来源:https://blog.csdn.net/Java_Yangxiaoyuan/article/details/135563494
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。