java8中HashMap扩容机制-结点的挂载

Posted You295

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java8中HashMap扩容机制-结点的挂载相关的知识,希望对你有一定的参考价值。

java8中HashMap扩容机制-结点的挂载

HashMap简介+成员变量

1.HashMap在底层数据结构上采用的是 :数组+链表+红黑树,通过散列映射储存键值对数 据,故查询时访问速度比较快;HashMap最多允许一对键值对的key值为null,允许多对键值对的value为null;排列无序,非线程安全。
2.Node<K,V>:链表的节点,包含了key,value,hash,next 四个元素。
3. size:记录元素的个数。
4.table:Node<K,v>类型的数组,里面元素为链表,储存元素。
5.LoadFactor:负载因子(0.75)。
6.threshold:阀值,决定HashMap在什么时候进行扩容,以及扩容后的大小。阀值=容量*负载因子

put方法

自己实现put方法代码,,仅仅适用于无扩容情况时,如下:

/**
	 * 添加数据
	 */
	@Override
	public void put(K key, V value) {
		int index = (key == null) ? 0 : key.hashCode() % table.length;// 数组的索引

		Entry<K, V> data = new Entry<>(key, value);
		Node<K, V> newNode = new Node<>(data, null);

		if (table[index] == null) { // 空链表无节点
			table[index] = newNode;
		} else {
			Node<K, V> tmp = table[index];
			while (tmp != null && tmp.next != null) { // 尾插法
				if (tmp.data.getKey() == key || tmp.data.getKey().equals(key)) {
					tmp.data.setValue(value);
					return;
				}
				tmp=tmp.next;
			}
			if (tmp.data.getKey() == key || tmp.data.getKey().equals(key)) {
				tmp.data.setValue(value);
				return;
			}
			tmp.next = newNode;
		}
		this.size++; // 容量大小+1
	}

源码如下:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
		Node<K, V>[] tab;//哈希表数组
		Node<K, V> p;//桶位置的头结点
		int n, i; //n:哈希表数组大小,,i:下标
		//当哈希表数组为null或者长度为0时,初始化哈希表数组
		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)
				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);
						//如果当前链表中元素大于数化的阀值,将链表转变为数
						if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
							treeifyBin(tab, hash);
						break;
					}
					//如果key-非头节点,已经存在,直接结束循环
					if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
						break;
					p = e;//重置p,用于遍历
				}
			}
			//如果key已经存在则更新value值
			if (e != null) { // existing mapping for key
				V oldValue = e.value;
				//更新当前的value值
				if (!onlyIfAbsent || oldValue == null)
					e.value = value;
				afterNodeAccess(e);
				return oldValue;
			}
		}
		++modCount;
		//如果键对个数大于阀值时,调用resize()方法,
		if (++size > threshold)
			resize();
		afterNodeInsertion(evict);
		return null;
	}
}

在源码putVal方法中第47和48 行代码可以看出,当添加完元素以后,如果HashMap发现size大于threshold(阀值)时,则会调用resize()方法进行扩容

java8中HashMap扩容需要满足的条件:当前数据存储的数量(size)大小必须大于等于阀值;

HashMap在存值时默认容量大小为16,,负载因子为0.75,,阀值为12。

扩容机制-resize方法

HashMap在三种情况下扩容:
1.使用默认构造方法初始化HashMap。从源码中可以看出HashMap在刚开始初始化的时候会返回一个空table,threshold值为0,所以第一次发生扩容的容量就是为默认值16,,并且threshold(阀值)为12=16*0.75

if ((tab = table) == null || (n = tab.length) == 0)
			n = (tab = resize()).length;

2.指定初始容量的构造方法初始化HashMap。源码中可以得到初始容量等于阀值。阀值=当前容量(threshold)*负载因子

3.HashMap不是第一次扩容,如已经扩容过了,那么每次table的容量以及threshold为原来的两倍。

扩容与插入元素的顺序: HashMap初始化后首次插入数据时,先resize扩容在插入数据,,之后每次插入数据的个数达到阀值的时候再扩容,这时则为先插入数据再扩容。

resize方法 源码如下:

Node<K, V>[] oldTab = table; // 记录原来的table
		// oldCap:原来数组的长度,,oldThr:原来table中节点个数的阀值
		// 如果当前数组等于null,长度返回0,否则返回当前数组长度
		int oldCap = (oldTab == null) ? 0 : oldTab.length;
		// 当前的阀值,默认为12
		int oldThr = threshold;
		int newCap, newThr = 0;
		if (oldCap > 0) {
			// 数组不能变大,则调大threshold,计算扩容后的大小
			if (oldCap >= MAXIMUM_CAPACITY) {
				// 如果超过了Int最大值就不在扩充了
				threshold = Integer.MAX_VALUE;
				return oldTab;
			}
			// 没有超过最大值,就扩容为原来的两倍
			// 1.(newCap = oldCap << 1) < MAXIMUM_CAPACITY 扩大两倍以后的容量小于最大容量()阀值
			// 2.oldCap >= DEFAULT_INITIAL_CAPACITY 原来数组长度大于等于数组初始化长度16
			else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
				newThr = oldThr << 1; // double threshold 阀值扩大一倍
		}
		// 原来阀值点大于0,直接赋值
		else if (oldThr > 0) // initial capacity was placed in threshold
			newCap = oldThr;// 老阀值赋值给新的数组长度
		else { // zero initial threshold signifies using defaults
			newCap = DEFAULT_INITIAL_CAPACITY; // 值为16
			newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
		}
		// 计算新resize的最大上限
		if (newThr == 0) {
			float ft = (float) newCap * loadFactor;
			newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ? (int) ft : Integer.MAX_VALUE);
		}
		// 新的阀值为扩容后的两倍,即为24
		threshold = newThr;
		// 创建新的哈希表
		@SuppressWarnings({ "rawtypes", "unchecked" })
		Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];// newCap是新的数组长度--32
		table = newTab; 新数组被创建出来后,table指向它
		// 判断原来数组是否为空
		if (oldTab != null) {
			// 把每个bucket都移动到新的bucket中去
			// 遍历原来哈希表的每一个桶,重新计算桶里元素的新位置
			for (int j = 0; j < oldCap; ++j) {
				Node<K, V> e;
				if ((e = oldTab[j]) != null) {
					// 将原来的数据赋值为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);
					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;
							// 判断如果等于true,e节点在resize之后不需要移动位置
							if ((e.hash & oldCap) == 0) {
								if (loTail == null)
									loHead = e;
								else
									loTail.next = e;
								loTail = e;
								// 原索引+oldCap
							} else {
								if (hiTail == null)
									hiHead = e;
								else
									hiTail.next = e;
								hiTail = e;
							}
						} while ((e = next) != null);
						// 将原索引放到bucket里
						if (loTail != null) {
							loTail.next = null;
							newTab[j] = loHead;
						}
						// 原索引+oldCap放到bucket里去
						if (hiTail != null) {
							hiTail.next = null;
							newTab[j + oldCap] = hiHead;
						}
					}
				}
			}
		}
		return newTab;
	}

知识点:
第一次添加元素的时候,初始化默认的长度为16,继续往map中加入元素,通过hash的值和数组的长度来决定放在数组的那个位置,,如果出现放在同一位置时,优先一链表的形式存放,在同一个位置存放的个数达到8个时(>=7),判断是否转变为红黑树;如果数组的长度还小于64,则会发生扩容数组,如果数组的长度大于等于64的时候才会将该结点的链表转变为数。扩容完成之后,如果某个节点的是数,同时该节点的个数小于等于6,则会将该数转变为链表。

以上是关于java8中HashMap扩容机制-结点的挂载的主要内容,如果未能解决你的问题,请参考以下文章

HashSet扩容成红黑树机制

HashMap的扩容机制

HashMap原理 — 扩容机制及存取原理

Java常见集合的默认大小及扩容机制

HashMap的扩容机制---resize()

HashMap何时扩容以及它的扩容机制?