JDK1.8中HashMap解析

一:概述

JDK 1.8对HashMap进行了比较大的优化,底层实现由之前的“数组+链表”改为“数组+链表+红黑树”

相对于JDK7中的HashMap,JDK8中HashMap的实现主要有一下几点不同:

  • 在JDK8中new HashMap()的时候,底层没有创建一个默认长度为16的Entry[]数组
  • 在JDK8中Entry[]数组变为Node[]数组
  • 在JDK8中首次调用put()方法是,底层创建长度为16的数组
  • 在JDK8中底层实现由之前的“数组+链表”改为“数组+链表+红黑树”,当数组上的某个索引的链表长度大于8切数组长度大于64时,链表变为红黑树,从而优化查询速度。(转为红黑树节点后,链表的结构还存在,通过next属性维持,红黑树节点在进行操作时都会维护链表的结构,并不是转为红黑树节点,链表结构就不存在了。)

二:构造方法

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//初始数组长度
static final int MAXIMUM_CAPACITY = 1 << 30;//最大数组长度
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认加载因子
static final int TREEIFY_THRESHOLD = 8;//链表转为红黑树时链表长度
static final int UNTREEIFY_THRESHOLD = 6;//红黑树转为链表时,红黑树中元素个数
static final int MIN_TREEIFY_CAPACITY = 64;//链表转为红黑树时,数组的长度
int threshold;//阈值,当存储元素大于阈值是,数组需要扩容,阈值=加载因子*数组长度
final float loadFactor;//记载因子
transient Node<K,V>[] table;//数组定义
transient int size;//保存元素个数

//空参构造
public HashMap() {
	this.loadFactor = DEFAULT_LOAD_FACTOR; 
}

//链表存储的节点
static class Node<K,V> implements Map.Entry<K,V> {
	final int hash;//键的hash值
	final K key;//键
	V value;//值
	Node<K,V> next;//下一个节点

	Node(int hash, K key, V value, Node<K,V> next) {
		this.hash = hash;
		this.key = key;
		this.value = value;
		this.next = next;
	}
	//......
}

//红黑树存储的节点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
	TreeNode<K,V> parent;  // 父节点
	TreeNode<K,V> left;//左子树
	TreeNode<K,V> right;//右子树
	TreeNode<K,V> prev; //删除后需要取消链接
	boolean red;//颜色属性
	TreeNode(int hash, K key, V val, Node<K,V> next) {
		super(hash, key, val, next);
	}
	
	//.....
}

二:put()方法

//添加数据
public V put(K key, V value) {
	return putVal(hash(key), key, value, false, true);
}

//对key进行hash处理
static final int hash(Object key) {
	int h;
	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

通过hash()计算key的hash值

1.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;
	//第一次调用时table为null   (tab = table) == null返回true
	if ((tab = table) == null || (n = tab.length) == 0)
		//通过resize()对数组扩容或初始化
		n = (tab = resize()).length;
	//(n - 1) & hash通过key的hash值判断应改存储的位置,如果tab[i = (n - 1) & hash]等于null,说明当前节点上不存值
	if ((p = tab[i = (n - 1) & hash]) == null)
		//新建一个节点给数组当前位置
		tab[i] = newNode(hash, key, value, null);
	else {//如果key的hash值计算的节点位置存在数据
		Node<K,V> e; K k;
		//判断这个节点存在的hash值与当前key的hash值是否相等
		//判断这个节点上的key是否等于当前要存储的key
		if (p.hash == hash &&
			((k = p.key) == key || (key != null && key.equals(k))))
			e = p;//如果第一次判断即链表的头结点的key就与当前要存储的key一样,就将当前节点赋值给e,留作后面替换value使用
		else if (p instanceof TreeNode)//如果当前节点是一个红黑树节点
			e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//加入到红黑树中
		else {//如果原有的hash值与当前key的hash值不相等
			for (int binCount = 0; ; ++binCount) {//循环遍历链表
				if ((e = p.next) == null) {//如果当前节点的下一个节点等于null
					p.next = newNode(hash, key, value, null);//新建一个node,将当前节点的下一个节点指向新建的节点
					if (binCount >= TREEIFY_THRESHOLD - 1) //如果链表长度大于等于7(8-1),则需要转换为红黑树
						treeifyBin(tab, hash);
					break;
				}
				//如果下一个节点的hash值等于当前key的hash值,则跳出循环
				if (e.hash == hash &&
					((k = e.key) == key || (key != null && key.equals(k))))
					break;
				p = e;//赋值p,用于下次循环
			}
		}
		if (e != null) { //e!=null说明当前要存储的key找到了与之相同的key的节点
			V oldValue = e.value;
			if (!onlyIfAbsent || oldValue == null)
				e.value = value;//替换value
			afterNodeAccess(e);
			return oldValue;//返回原有value
		}
	}
	++modCount;
	if (++size > threshold)//如果存储的元素大于阈值
		resize();//扩容
	afterNodeInsertion(evict);
	return null;
}
  • 校验table是否为空或者length等于0,如果是则调用resize方法进行初始化或扩容
  • 通过hash值计算索引位置,将该索引位置的头节点赋值给p节点,如果该索引位置节点为空则使用传入的参数新增一个节点并放在该索引位置
  • 判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点即为要查找的目标节点,将p节点赋值给e节点
  • 如果p节点不是目标节点,则判断p节点是否为TreeNode,如果是则调用红黑树的putTreeVal方法查找目标节点
  • 走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,并定义变量binCount来统计该链表的节点数
  • 如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部,并校验节点数是否超过8个,如果超过则调用treeifyBin方法将链表节点转为红黑树节点
  • 如果遍历的e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
  • 如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue
  • 如果插入节点后节点数超过阈值,则调用resize方法进行扩容

2.resize()方法,初始化或扩容数组

//扩容或初始化数组
final Node<K,V>[] resize() {
	Node<K,V>[] oldTab = table;//获取数组,初始时为null
	int oldCap = (oldTab == null) ? 0 : oldTab.length;//如果数组为null,oldCap=0,否则等于数组长度
	int oldThr = threshold;//获得阈值,初始时,没有对阈值进行操作,所以为0
	int newCap, newThr = 0;
	if (oldCap > 0) {//初始时oldCap=0,if进不去
		if (oldCap >= MAXIMUM_CAPACITY) {//如果数组长度>=2^30
			threshold = Integer.MAX_VALUE;//阈值就等于Integer的最大值
			return oldTab;//返回数组
		}
		//如果数组长度的2倍<2^30,数组长度>=16
		else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
				 oldCap >= DEFAULT_INITIAL_CAPACITY)
			newThr = oldThr << 1; // 阈值为数组的2倍
	}
	else if (oldThr > 0) //初始时阈值等于0,if进不去
		newCap = oldThr;
	else { //初始时阈值等于0,进入if
		newCap = DEFAULT_INITIAL_CAPACITY;//赋值初始数组大小及阈值
		newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//阈值=加载因子*数组长度
	}
	if (newThr == 0) {//阈值等于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];//创建数组,初始长度=16,之后的长度有计算得来
	table = newTab;//赋值给成员变量table
	if (oldTab != null) {//数组不等于null
		for (int j = 0; j < oldCap; ++j) {//循环数组长度
			Node<K,V> e;
			if ((e = oldTab[j]) != null) {//数组的第j个节点不等于null
				oldTab[j] = null;//指定为null
				if (e.next == null)//为null表示数组的这个节点就只有一个节点,不是一个链表
					//e.hash & (newCap - 1)通过hash值计算存储位置,并在新的数组的这个位置上赋值为循环的当前节点
					newTab[e.hash & (newCap - 1)] = e;
				else if (e instanceof TreeNode)//如果这个节点是红黑树节点
					((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
				else { //数组的j节点是一个链表
					Node<K,V> loHead = null, loTail = null;
					Node<K,V> hiHead = null, hiTail = null;
					Node<K,V> next;
					do {
						next = e.next;//获得当前节点的下一个节点
						//如果当前节点的hash值与老数组的容量进行与运算为0,则扩容后的索引位置跟老数组的索引位置一样
						if ((e.hash & oldCap) == 0) {
							if (loTail == null)//如果loTail为null,则当前节点为第一个节点
								loHead = e;//设置头结点
							else
								loTail.next = e;//如果不是第一个节点,则上一个节点的下一个节点为当前节点
							loTail = e;
						}
						//如果当前节点的hash值与老数组的容量进行与运算为1,则扩容后的索引位置为:老表的索引位置+oldCap
						else {
							if (hiTail == null)
								hiHead = e;//设置头节点
							else
								hiTail.next = e;//上一个节点的下一个节点为当前节点
							hiTail = e;
						}
					} while ((e = next) != null);//循环链表
					if (loTail != null) {
						loTail.next = null;//最后一个节点的next设置为null
						newTab[j] = loHead;//设置新数组的j位置为loHead
					}
					if (hiTail != null) {
						hiTail.next = null;//最后一个节点的next设置为null
						newTab[j + oldCap] = hiHead;//设置新数组的j+oldCap位置为hiHead
					}
				}
			}
		}
	}
	return newTab;//返回新数组
}
  • 如果老数组的容量大于0,判断老数组的容量是否超过最大容量值:如果超过则将阈值设置为Integer.MAX_VALUE,并直接返回老数组(此时oldCap * 2比Integer.MAX_VALUE大,因此无法进行重新分布,只是单纯的将阈值扩容到最大);将新数组的容量赋值为老数组的容量*2,如果新容量小于最大容量并且老容量不小于16,则直接将新的阈值设置为原来的两倍。
  • 如果老数组的容量为0,老数组的阈值大于0,这种情况是传了容量的new方法创建的空表,将新数组的容量设置为老数组的阈值(这种情况发生在新创建的HashMap第一次put时,该HashMap初始化的时候传了初始容量,由于HashMap并没有capacity变量来存放容量值,因此传进来的初始容量是存放在threshold变量上(查看HashMap(int initialCapacity, float loadFactor)方法),因此此时老数组的threshold的值就是我们要新创建的HashMap的capacity,所以将新数组的容量设置为老数组的阈值。
  • 如果老数组的容量为0,老数组的阈值为0,这种情况是没有传容量的new方法创建的空表,将阈值和容量设置为默认值。
  • 如果新数组的阈值为空,则通过新的容量 * 负载因子获得阈值(这种情况是初始化的时候传了初始容量,跟第2点相同情况,或者初始容量设置的太小导致老数组的容量没有超过16导致的)。
  • 将当前阈值设置为刚计算出来的新的阈值,定义新数组,容量为刚计算出来的新容量,将当前的表设置为新定义的表。
  • 如果老数组不为空,则需遍历所有节点,将节点赋值给新数组。
  • 将老数组上索引为j的头结点赋值给e节点,并将老数组上索引为j的节点设置为空。
  • 如果e的next节点为空,则代表老数组的该位置只有1个节点,通过hash值计算新数组的索引位置,直接将该节点放在新数组的该位置上。
  • 如果e的next节点不为空,并且e为TreeNode,则调用split方法(见下文代码块10)进行hash分布。
  • 如果e的next节点不为空,并且e为普通的链表节点,则进行普通的hash分布。
  • 如果e的hash值与老数组的容量(为一串只有1个为2的二进制数,例如16为0000 0000 0001 0000)进行位与运算为0,则说明e节点扩容后的索引位置跟老数组的索引位置一样(见例子1),进行链表拼接操作:如果loTail为空,代表该节点为第一个节点,则将loHead赋值为该节点;否则将节点添加在loTail后面,并将loTail赋值为新增的节点。
  • 如果e的hash值与老数组的容量(为一串只有1个为2的二进制数,例如16为0000 0000 0001 0000)进行位与运算为1,则说明e节点扩容后的索引位置为:老数组的索引位置+oldCap(见例子1),进行链表拼接操作:如果hiTail为空,代表该节点为第一个节点,则将hiHead赋值为该节点;否则将节点添加在hiTail后面,并将hiTail赋值为新增的节点。
  • 老数组节点重新hash分布在新数组结束后,如果loTail不为空(说明老数组的数据有分布到新数组上原索引位置的节点),则将最后一个节点的next设为空,并将新数组上原索引位置的节点设置为对应的头结点;如果hiTail不为空(说明老数组的数据有分布到新数组上原索引+oldCap位置的节点),则将最后一个节点的next设为空,并将新数组上索引位置为原索引+oldCap的节点设置为对应的头结点。
  • 返回新数组。

3.putTreeVal()方法,将当前key加入到红黑树中,putTreeVal是TreeNode中的方法

//将当前key加入到红黑树中,putTreeVal是TreeNode中的方法
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)//当前节点的hash值是否大于要加入的key的hash(左)
			dir = -1;
		else if (ph < h)//当前节点的hash值是否小于要加入的key的hash(右)
			dir = 1;
		else if ((pk = p.key) == k || (k != null && k.equals(pk)))//当前节点的hash值是否等于于要加入的key的hash
			return p;//相等,说明当前可重复,返回这个节点即可
		// 如果key所属的类没有实现Comparable接口 或者 key和当前节点的key相等
		else if ((kc == null &&
				  (kc = comparableClassFor(k)) == null) ||
				 (dir = compareComparables(kc, k, pk)) == 0) {
			if (!searched) {//循环中第一次进入才执行
				TreeNode<K,V> q, ch;
				searched = true;
				 // 从当前节点的左节点和右节点分别调用find方法进行查找, 如果查找到目标节点则返回
				if (((ch = p.left) != null &&
					 (q = ch.find(h, k, kc)) != null) ||
					((ch = p.right) != null &&
					 (q = ch.find(h, k, kc)) != null))
					return q;
			}
			// 否则使用定义的一套规则来比较当前key和当前节点节点的key的大小, 用来决定向左还是向右查找
			dir = tieBreakOrder(k, pk);
		}

		TreeNode<K,V> xp = p;
		if ((p = (dir <= 0) ? p.left : p.right) == null) {//通过上面的比较,来获取当前节点的左或右节点,在判断其是否等于null,null说明遍历到叶子节点,否则继续遍历
			Node<K,V> xpn = xp.next;//获得当前节点的下一个节点
			TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);//通过当前HashMap对象创建一个新的红黑树节点
			if (dir <= 0)//新节点加入到当前节点的左边
				xp.left = x;
			else//新节点加入到当前节点的右边
				xp.right = x;
			xp.next = x;//指定下一个节点
			x.parent = x.prev = xp;//将新节点的parent和prev节点设置为当前节点
			if (xpn != null)// 如果当前节点的下一个节点不为空,则将其prev节点设置为新建节点,与上文的新建节点的next节点对应
				((TreeNode<K,V>)xpn).prev = x;
			moveRootToFront(tab, balanceInsertion(root, x));// 进行红黑树的插入平衡调整
			return null;
		}
	}
}
  • 查找当前红黑树的根结点,将根结点赋值给p节点,开始进行查找
  • 如果传入的hash值小于p节点的hash值,将dir赋值为-1,代表向p的左边查找树
  • 如果传入的hash值大于p节点的hash值, 将dir赋值为1,代表向p的右边查找树
  • 如果传入的hash值等于p节点的hash值,并且传入的key值跟p节点的key值相等, 则该p节点即为目标节点,返回p节点
  • 如果k所属的类没有实现Comparable接口,或者k和p节点的key使用compareTo方法比较相等:第一次会从p节点的左节点和右节点分别调用find方法进行查找,如果查找到目标节点则返回;如果不是第一次或者调用find方法没有找到目标节点,则调用tieBreakOrder方法比较k和p节点的key值的大小,以决定向树的左节点还是右节点查找。
  • 如果dir <= 0则向左节点查找(p赋值为p.left,并进行下一次循环),否则向右节点查找,如果已经无法继续查找(p赋值后为null),则代表该位置即为x的目标位置,另外变量xp用来记录查找的最后一个节点,即下文新增的x节点的父节点。
  • 以传入的hash、key、value参数和xp节点的next节点为参数,构建x节点(注意:xp节点在此处可能是叶子节点、没有左节点的节点、没有右节点的节点三种情况,即使它是叶子节点,它也可能有next节点,红黑树的结构跟链表的结构是互不影响的,不会因为某个节点是叶子节点就说它没有next节点,红黑树在进行操作时会同时维护红黑树结构和链表结构,next属性就是用来维护链表结构的),根据dir的值决定x决定放在xp节点的左节点还是右节点,将xp的next节点设为x,将x的parent和prev节点设为xp,如果原xp的next节点(xpn)不为空, 则将该节点的prev节点设置为x节点, 与上面的将x节点的next节点设置为xpn对应。
  • 进行红黑树的插入平衡调整

4.split()方法,TreeNode中的方法,参数依次为 HashMap对象,新数组,红黑树在老数组中的索引,老数组大小

//TreeNode中的方法,参数依次为 HashMap对象,新数组,红黑树在老数组中的索引,老数组大小
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
	TreeNode<K,V> b = this;//老数组index索引下的第一个节点
	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;
		//如果当前节点的hash值与老数组的容量进行与运算为0,则扩容后的索引位置跟老数组的索引位置一样
		if ((e.hash & bit) == 0) {
			if ((e.prev = loTail) == null)// 如果loTail为空, 代表该节点为第一个节点
				loHead = e;//当前节点指向头结点
			else
				loTail.next = e;//上一个节点的下一个节点等于当前循环到的节点
			loTail = e;
			++lc;
		}
		//如果当前节点的hash值与老数组的容量进行与运算为1,则扩容后的索引位置为:老表的索引位置+oldCap
		else {
			if ((e.prev = hiTail) == null)//如果hiTail为空, 代表该节点为第一个节点
				hiHead = e;//当前节点指向头结点
			else
				hiTail.next = e;//上一个节点的下一个节点等于当前循环到的节点
			hiTail = e;
			++hc;
		}
	}

	if (loHead != null) {
		if (lc <= UNTREEIFY_THRESHOLD)//如果当前红黑树节点<=6
			tab[index] = loHead.untreeify(map);//将红黑树转换为链表,赋值到新数组的index位置
		else {
			tab[index] = loHead;//新数组的index节点指向loHead
			if (hiHead != null) //
				loHead.treeify(tab);//构建红黑树
		}
	}
	if (hiHead != null) {
		if (hc <= UNTREEIFY_THRESHOLD)//如果当前红黑树节点<=6
			tab[index + bit] = hiHead.untreeify(map);//将红黑树转换为链表,赋值到新数组的(index+老数组长度)位置
		else {
			tab[index + bit] = hiHead;//将hiHead赋值到新数组的(index+老数组长度)位置
			if (loHead != null)
				hiHead.treeify(tab);//构建红黑树
		}
	}
}
  • 以调用此方法的节点开始,遍历整个红黑树节点(此处实际是遍历的链表节点)。
  • 如果e的hash值与老表的容量(为一串只有1个为2的二进制数,例如16为0000 0000 0001 0000)进行位与运算为0,则说明e节点扩容后的索引位置跟老表的索引位置一样,进行链表拼接操作:如果loTail为空,代表该节点为第一个节点,则将loHead赋值为该节点;否则将节点添加在loTail后面,并将loTail赋值为新增的节点,并统计原索引位置的节点个数。
  • 如果e的hash值与老表的容量(为一串只有1个为2的二进制数,例如16为0000 0000 0001 0000)进行位与运算为1,则说明e节点扩容后的索引位置为:老表的索引位置+oldCap,进行链表拼接操作:如果hiTail为空,代表该节点为第一个节点,则将hiHead赋值为该节点;否则将节点添加在hiTail后面,并将hiTail赋值为新增的节点,并统计索引位置为原索引+oldCap的节点个数。
  • 如果原索引位置的节点不为空:如果当该索引位置节点数<=6个,调用untreeify方法将红黑树节点转为链表节点;否则将原索引位置的节点设置为对应的头结点(即loHead结点),如果判断hiHead不为空则代表原来的红黑树(老表的红黑树由于节点被分到两个位置)已经被改变,需要重新构建新的红黑树,以loHead为根结点,调用treeify方法构建新的红黑树。
  • 如果索引位置为原索引+oldCap的节点不为空:如果当该索引位置节点数<=6个,调用untreeify方法将红黑树节点转为链表节点;否则将索引位置为原索引+oldCap的节点设置为对应的头结点(即hiHead结点),如果判断loHead不为空则代表原来的红黑树(老表的红黑树由于节点被分到两个位置)已经被改变,需要重新构建新的红黑树,以hiHead为根结点,调用treeify方法构建新的红黑树。

5.untreeify()方法,TreeNode中的方法,将红黑树转换为链表,参数为HashMap对象

//TreeNode中的方法,将红黑树转换为链表,参数为HashMap对象
final Node<K,V> untreeify(HashMap<K,V> map) {
	Node<K,V> hd = null, tl = null;
	//this为红黑树头结点
	for (Node<K,V> q = this; q != null; q = q.next) {
		// 调用replacementNode方法构建链表节点
		Node<K,V> p = map.replacementNode(q, null);
		if (tl == null)//第一次循t1为null
			hd = p;//当前节点为头结点
		else
			tl.next = p;//上一个节点的下一个节点为当前节点
		tl = p;
	}
	return hd;//返回头节点
}

6.treeifyBin()方法,TreeNode中的方法,将链表转换为红黑树

//将链表转换为红黑树
final void treeifyBin(Node<K,V>[] tab, int hash) {
	int n, index; Node<K,V> e;
	//数组等于null或者数组长度<64
	if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
		resize();//扩容,只有链表长度>8并且数组长度大于64才变为红黑树,防止数组长度<64变为红黑树
	else if ((e = tab[index = (n - 1) & hash]) != null) {//如果过当前key要存储在数组上的对应的位置存在节点
		TreeNode<K,V> hd = null, tl = null;
		do {
			TreeNode<K,V> p = replacementTreeNode(e, null);//根据当前链表节点,创建树节点
			if (tl == null)//等于null表示虚幻第一次进入
				hd = p;//新的节点设置为头结点
			else {
				p.prev = tl;//当前新建的节点的prev设置为上一个建的节点
				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);
}
  • 校验table是否为空,如果长度小于64,则调用resize方法进行扩容。
  • 根据hash值计算索引值,将该索引位置的节点赋值给e节点,从e节点开始遍历该索引位置的链表。
  • 调用replacementTreeNode方法(该方法就一行代码,直接返回一个新建的TreeNode)将链表节点转为红黑树节点,将头结点赋值给hd节点,每次遍历结束将p节点赋值给tl,用于在下一次循环中作为上一个节点进行一些链表的关联操作(p.prev = tl 和 tl.next = p)。
  • 将table该索引位置赋值为新转的TreeNode的头节点hd,如果该节点不为空,则以hd为根结点,调用treeify方法构建红黑树。

7.treeify()方法,ThreeNode中方法,构建红黑树

//构建红黑树
final void treeify(Node<K,V>[] tab) {
	TreeNode<K,V> root = null;
	//获取在treeifyBi方法的do-while中创建头节点hd
	for (TreeNode<K,V> x = this, next; x != null; x = next) {
		next = (TreeNode<K,V>)x.next;//获取下一个节点
		x.left = x.right = null;//指定其左右子节点都为null
		if (root == null) {//第一次循环root为null
			x.parent = null;//hd的parent为null
			x.red = false;//hd为黑节点
			root = x;//将根节点指向hd
		}
		else {//第二次及其之后循环进入
			K k = x.key;//获得节点key
			int h = x.hash;//获得节点hash码
			Class<?> kc = null;
			for (TreeNode<K,V> p = root;;) {
				int dir, ph;
				K pk = p.key;//获得根节点的key
				if ((ph = p.hash) > h)//根节点的hash码>当前节点的hash
					dir = -1;
				else if (ph < h)//根节点的hash码<当前节点的hash
					dir = 1;
				else if ((kc == null &&
						  (kc = comparableClassFor(k)) == null) ||
						 (dir = compareComparables(kc, k, pk)) == 0)
					// 否则使用定义的一套规则来比较当前key和当前节点节点的key的大小, 用来决定向左还是向右查找
					dir = tieBreakOrder(k, pk);

				TreeNode<K,V> xp = p;//保存根节点
				//通过dir获取根节点的左或右节点,并且左或右节点等于null,不等于null继续循环
				if ((p = (dir <= 0) ? p.left : p.right) == null) {
					x.parent = xp;//当前循环到的节点的parent等于根节点
					if (dir <= 0)
						xp.left = x;//根节点的左节点等于当前节点
					else
						xp.right = x;//根节点的右节点等于当前节点
					// 进行红黑树的插入平衡(通过左旋、右旋和改变节点颜色来保证当前树符合红黑树的要求)
					root = balanceInsertion(root, x);
					break;
				}
			}
		}
	}
	moveRootToFront(tab, root);// 如果root节点不在table索引位置的头结点, 则将其调整为头结点
}
  • 从调用此方法的节点作为起点,开始进行遍历,并将此节点设为root节点,标记为黑色(x.red = false)。
  • 如果当前节点不是根结点,则从根节点开始查找属于该节点的位置(该段代码跟之前的代码块2和代码块4的查找代码类似)。
  • 如果x节点(将要插入红黑树的节点)的hash值小于p节点(当前遍历到的红黑树节点)的hash值,则向p节点的左边查找。
  • 与3相反,如果x节点的hash值大于p节点的hash值,则向p节点的右边查找。
  • 如果x的key没有实现Comparable接口,或者x节点的key和p节点的key相等,使用tieBreakOrder方法来比较x节点和p节点的大小,以决定向左还是向右查找(dir <= 0向左,否则向右)。
  • 如果dir <= 0则向左节点查找(p赋值为p.left,并进行下一次循环),否则向右节点查找,如果已经无法继续查找(p赋值后为null),则代表该位置即为x的目标位置,另外变量xp用来记录最后一个节点,即为下文新增的x节点的父节点。
  • 将x的父节点设置为xp,根据dir的值决定x决定放在xp节点的左节点还是右节点,最后进行红黑树的插入平衡调整。
  • 调用moveRootToFront方法将root节点调整到索引位置的头结点。

8.moveRootToFront()方法,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) {
		//计算root及诶单的hashcode对应的存储位置
		int index = (n - 1) & root.hash;
		//获得这个位置上的头节点
		TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
		if (root != first) {//如果这个节点不等于root
			Node<K,V> rn;
			tab[index] = root;//设置头节点为root
			TreeNode<K,V> rp = root.prev;//获得root节点上一个节点
			if ((rn = root.next) != null)//如果root节点的next节点不等于null
				((TreeNode<K,V>)rn).prev = rp;//root的下一个节点的上一个节点等于root节点的上一个节点
			if (rp != null)
				rp.next = rn;//root的上一个节点的next等于root的下一个节点
			if (first != null)
				first.prev = root;//原来头节点的上一个节点为root
			root.next = first;//root的next为原来头节点
			root.prev = null;
		}
		assert checkInvariants(root); // 检查树是否正常
	}
}

//校验树的合法性:将传入的节点作为根结点,遍历所有节点,校验节点的合法性,主要是保证该树符合红黑树的规则。
static <K,V> boolean checkInvariants(TreeNode<K,V> t) {
		TreeNode<K,V> tp = t.parent, tl = t.left, tr = t.right,
			tb = t.prev, tn = (TreeNode<K,V>)t.next;
		if (tb != null && tb.next != t)
			return false;
		if (tn != null && tn.prev != t)
			return false;
		if (tp != null && t != tp.left && t != tp.right)
			return false;
		if (tl != null && (tl.parent != t || tl.hash > t.hash))
			return false;
		if (tr != null && (tr.parent != t || tr.hash < t.hash))
			return false;
		if (t.red && tl != null && tl.red && tr != null && tr.red)
			return false;
		if (tl != null && !checkInvariants(tl))
			return false;
		if (tr != null && !checkInvariants(tr))
			return false;
		return true;
	}
}
  • 校验root是否为空、table是否为空、table的length是否大于0。
  • 根据root节点的hash值计算出索引位置,判断该索引位置的头节点是否为root节点,如果不是则进行以下操作将该索引位置的头结点替换为root节点。
  • 将该索引位置的头结点赋值为root节点,如果root节点的next节点不为空,则将root节点的next节点的prev属性设置为root节点的prev节点。
  • 如果root节点的prev节点不为空,则将root节点的prev节点的next属性设置为root节点的next节点(3和4两个操作是一个完整的链表移除某个节点过程)。
  • 如果原头节点不为空,则将原头节点的prev属性设置为root节点
  • 将root节点的next属性设置为原头节点(5和6两个操作将first节点接到root节点后面)
  • root此时已经被放到该位置的头结点位置,因此将prev属性设为空。
  • 调用checkInvariants方法检查树是否正常。

三:get()方法

1.getNode()方法

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

final Node<K,V> getNode(int hash, Object key) {
	Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
	//数组不等于null,数组长度大于0,通过hash计算的位置存在节点
	if ((tab = table) != null && (n = tab.length) > 0 &&
		(first = tab[(n - 1) & hash]) != null) {
		if (first.hash == hash && //第一个节点的hash等于要获取的key的hash
			((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;
}
  • 先对table进行校验,校验是否为空,length是否大于0
  • 使用table.length - 1和hash值进行位与运算,得出在table上的索引位置,将该索引位置的节点赋值给first节点,校验该索引位置是否为空
  • 检查first节点的hash值和key是否和入参的一样,如果一样则first即为目标节点,直接返回first节点
  • 如果first的next节点不为空则继续遍历
  • 如果first节点为TreeNode,则调用getTreeNode方法查找目标节点
  • 如果first节点不为TreeNode,则调用普通的遍历链表方法查找目标节点
  • 如果查找不到目标节点则返回空

2.find()方法,TreeNode中方法

//通过key的hash及key获取节点
final TreeNode<K,V> getTreeNode(int h, Object k) {
	//parent不等于null就获取根节点,否者就是数组中通过hash计算的那个位置的节点,调用其find查找
	return ((parent != null) ? root() : this).find(h, k, null);
}

//通过key的hash及key查找节点
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)//当前节点的hash>要查询的key的hash
			p = pl;//p=根节点的左节点
		else if (ph < h)//当前节点的hash<要查询的key的hash
			p = pr;//p=根节点的右节点
		else if ((pk = p.key) == k || (k != null && k.equals(pk)))//当前节点的hash=要查询的key的hash
			return p;//查到,返回当前节点
		else if (pl == null)
			p = pr;// p节点的左节点为空则将向右遍历
		else if (pr == null)
			p = pl;// p节点的右节点为空则将向左遍历
		else if ((kc != null ||
					// 如果传入的key所属的类实现了Comparable接口,则将传入的key跟p节点的key比较
				  (kc = comparableClassFor(k)) != null) &&// 此行不为空代表key实现了Comparable
				 (dir = compareComparables(kc, k, pk)) != 0)//k<pk则dir<0, k>pk则dir>0
			p = (dir < 0) ? pl : pr;// k < pk则向左遍历(p赋值为p的左节点), 否则向右遍历
		else if ((q = pr.find(h, k, kc)) != null)//key所属类没有实现Comparable, 直接指定向p的右边遍历
			return q;
		else//上一个向右遍历(pr.find(h, k, kc))为空, 因此直接向左遍历
			p = pl;
	} while (p != null);
	return null;//循环没找到,直接返回null
}
  • 将p节点赋值为调用此方法的节点
  • 如果传入的hash值小于p节点的hash值,则往p节点的左边遍历
  • 如果传入的hash值大于p节点的hash值,则往p节点的右边遍历
  • 如果传入的hash值等于p节点的hash值,并且传入的key值跟p节点的key值相等, 则该p节点即为目标节点,返回p节点
  • 如果p的左节点为空则向右遍历,反之如果p的右节点为空则向左遍历
  • 如果传入的key(即代码中的参数变量k)所属的类实现了Comparable接口(kc不为空,comparableClassFor方法见下文代码块3),则将传入的key跟p节点的key进行比较(kc实现了Comparable接口,因此通过kc的比较方法进行比较),并将比较结果赋值给dir,如果dir<0则代表k<pk,则向p节点的左边遍历(pl);否则,向p节点的右边遍历(pr)。
  • 代码走到此处,代表key所属类没有实现Comparable,因此直接指定向p的右边遍历,如果能找到目标节点则返回
  • 代码走到此处代表与第7点向右遍历没有找到目标节点,因此直接向左边遍历
  • 以上都找不到目标节点则返回空

四:remove()方法

1.removeNode()方法,主要进行链表节点的移除

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;
	//数组!=null,数组大小>0,key的hash值对应的位置存在数据
	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对应的节点
		if (p.hash == hash &&
			((k = p.key) == key || (key != null && key.equals(k))))
			node = p;
		else if ((e = p.next) != null) {//头结点下一个节点不等null
			if (p instanceof TreeNode)//如果是红黑树节点
				node = ((TreeNode<K,V>)p).getTreeNode(hash, key);//获取节点
			else {//是链表
				do {
					//当前循环的节点的key就是要找的key
					if (e.hash == hash &&
						((k = e.key) == key ||
						 (key != null && key.equals(k)))) {
						node = e;//赋值node,后面使用
						break;
					}
					p = e;
				} while ((e = e.next) != null);//循环链表
			}
		}
		//node不等以null就直接进入
		if (node != null && (!matchValue || (v = node.value) == value ||
							 (value != null && value.equals(v)))) {
			if (node instanceof TreeNode)//如果是红黑树节点
				((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);//移除节点
			else if (node == p)//node如果是第一个节点
				tab[index] = node.next;//node的下一个节点指向第一个节点
			else
				p.next = node.next;//p的下一个节点指向node的下一个节点,移除node
			++modCount;
			--size;
			afterNodeRemoval(node);
			return node;
		}
	}
	return null;
}
  • 如果table不为空并且根据hash值计算出来的索引位置的值不为空,将该位置的节点赋值给p。
  • 如果p节点的hash值和key都与传入的相同,则p即为目标节点,赋值给node。
  • 向下遍历节点,如果p是TreeNode则调用getTreeNode方法查找节点,并赋值给node。
  • 遍历链表查找符合条件的节点,当节点的hash值和key与传入的值相同,则该节点即为目标节点, 赋值给node,并跳出循环。
  • 如果node不为空,即根据传入key和hash值查找到目标节点,判断node是否为TreeNode,如果是则调用红黑树的移除方法removeTreeNode方法。
  • 如果node是该索引位置的头结点则直接将该索引位置的值赋值为node节点的next节点。
  • 否则将node的上一个节点(p节点)的next节点设置为node的next节点,即将node节点移除,将node的上下节点进行关联。

2.removeTreeNode()方法,进行红黑树节点的移除

//移除红黑树节点,参数 HashMap对象,数组,true,方法中的this为要移除的节点对象
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                                  boolean movable) {
	int n;
	if (tab == null || (n = tab.length) == 0)
		return;
	int index = (n - 1) & hash;//计算当前节点的hash锁存储的位置,hash为this的成员变量
	TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;//获得根节点
	TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;//获取当前节点的下一个节点及prev节点
	if (pred == null)//prev节点等于null
		tab[index] = first = succ;//下一个节点指向数组对应索引位置
	else
		pred.next = succ;//prev节点的下一个节点为当前要移除节点的下一个节点
	if (succ != null)//当前要移除节点的下一个节点不等于null
		succ.prev = pred;//当前要移除节点下一个节点的prev节点等于当前要移除节点的prev节点
	if (first == null)//头结点为null,则表述红黑树没有节点了,直接return
		return;
	if (root.parent != null)//根节点的父节点不等于null
		root = root.root();//获取根节点(索引位置不一定为红黑树根节点)
	if (root == null || root.right == null ||
		(rl = root.left) == null || rl.left == null) {
		tab[index] = first.untreeify(map);  // 将红黑树转换为链表
		return;
	}
	//以上红黑树的链表部分就处理完毕,要移除的节点已经从链表中移除(红黑树中依旧保留了链表的引用)
	//获取要移除的节点,左右子节点
	TreeNode<K,V> p = this, pl = left, pr = right, replacement;
	if (pl != null && pr != null) {//左右节点不为null
		TreeNode<K,V> s = pr, sl;
		while ((sl = s.left) != null) //右节点的左节点不等于null ,向左一直查找到叶子节点
			s = sl;
		boolean c = s.red; s.red = p.red; p.red = c; // 交换要移除节点和叶子节点的颜色
		TreeNode<K,V> sr = s.right;//s的右节点
		TreeNode<K,V> pp = p.parent;//要移除节点的父节点
		
		//第一次移动(代码过程可以参考下图)
		if (s == pr) { 
			p.parent = s;
			s.right = p;
		}
		else {
			TreeNode<K,V> sp = s.parent;
			if ((p.parent = sp) != null) {
				if (s == sp.left)
					sp.left = p;
				else
					sp.right = p;
			}
			if ((s.right = pr) != null)
				pr.parent = s;
		}
		//第二次移动(代码过程可以参考下图)
		p.left = null;
		if ((p.right = sr) != null)
			sr.parent = p;
		if ((s.left = pl) != null)
			pl.parent = s;
		if ((s.parent = pp) == null)
			root = s;
		else if (p == pp.left)
			pp.left = s;
		else
			pp.right = s;
		if (sr != null)
			replacement = sr;
		else
			replacement = p;
	}
	else if (pl != null)
		replacement = pl;
	else if (pr != null)
		replacement = pr;
	else
		replacement = p;			
	//第三次移动(代码过程可以参考下图)
	if (replacement != p) {
		TreeNode<K,V> pp = replacement.parent = p.parent;
		if (pp == null)
			root = replacement;
		else if (p == pp.left)
			pp.left = replacement;
		else
			pp.right = replacement;
		p.left = p.right = p.parent = null;
	}

	// 如果p节点不为红色则进行红黑树删除平衡调整(如果删除的节点是红色则不会破坏红黑树的平衡无需调整)
	TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);

	if (replacement == p) {  //如果p节点为叶子节点, 则简单的将p节点去除即可
		TreeNode<K,V> pp = p.parent;
		p.parent = null;
		if (pp != null) {
			if (p == pp.left)
				pp.left = null;
			else if (p == pp.right)
				pp.right = null;
		}
	}
	if (movable)
		moveRootToFront(tab, r);//将root节点移到索引位置的头结点
}

五:死循环问题

在JDK1.7中HashMap在扩容后,新的链表与原有链表会发生顺序颠倒的情况,从而导致在多线程时容易发生死循环的问题(死循环问题分析https://coolshell.cn/articles/9606.html),那么在JDK1.8中对这种为题进行了优化

在resize()方法中,发现在扩容四对老的链表节点进行分类,从而使扩容后新的链表依旧是原来的顺序,因此,即使此时有多个线程并发扩容,也不会出现死循环的情况。

六:总结

  • HashMap的底层是个Node数组(Node<K,V>[] table),在数组的具体索引位置,如果存在多个节点,则可能是以链表或红黑树的形式存在。
  • 增加、删除、查找键值对时,定位到在数组的索引位置是很关键的一步,源码中是通过下面3个操作来完成这一步:1)拿到key的hashCode值;2)将hashCode的高位参与运算,重新计算hash值;3)将计算出来的hash值与(table.length - 1)进行&运算。
  • HashMap中数组默认初始容量是16,数组容量必须为2的幂次方,每次扩容原来的2倍;默认加载因子是0.75;实际能存放的节点个数(即触发扩容的阈值)= 数组容量* 加载因子。
  • HashMap在触发扩容后,阈值会变为原来的2倍,并且会进行重hash,重hash后索引位置index的节点的新分布位置最多只有两个:原索引位置原索引+原数组长度位置。例如原数组长度为16,索引位置5的节点扩容后,只可能分布在新报索引位置5和索引位置21(5+16)。
  • 导致HashMap扩容后,同一个索引位置的节点重hash最多分布在两个位置的根本原因是:1)table的长度始终为2的n次方;2)索引位置的计算方法为“(table.length - 1) & hash”。HashMap扩容是一个比较耗时的操作,定义HashMap时尽量给个接近的初始容量值。
  • HashMap有threshold属性和loadFactor属性,但是没有capacity属性。初始化时,如果传了初始化容量值,该值是存在threshold变量,并且Node数组是在第一次put时才会进行初始化,初始化时会将此时的threshold值作为新表的capacity值,然后用capacity和loadFactor计算新表的真正threshold值。
  • 当同一个索引位置的节点在增加后达到8个时,并且此时数组的长度大于等于64,则会触发链表节点(Node)转红黑树节点(TreeNode,间接继承Node),转成红黑树节点后,其实链表的结构还存在,通过next属性维持。链表节点转红黑树节点的具体方法为源码中的treeifyBin(Node<K,V>[] tab, int hash)方法。而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容。
  • 当同一个索引位置的节点在移除后达到6个时,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点。红黑树节点转链表节点的具体方法为源码中的untreeify(HashMap<K,V> map)方法。
  • HashMap在JDK1.8之后不再有死循环的问题,JDK1.8之前存在死循环的根本原因是在扩容后同一索引位置的节点顺序会反掉。
  • HashMap是非线程安全的,在并发场景下使用ConcurrentHashMap来代替。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值