2021.01.29 第一阶段 15

1.HashMap的源码分析
2.HashSet的源码分析

一、HashMap的源码分析

(一) 概述
1、HashMap出现在JDK1.2版本,到JDK7版本为止,都没有产生过较大的变化
2、HashMap在JDK8版本产生了较大的变化
(1)HashMap在JDK7版本的时候,底层数据结构是【数组+链表】
(2)HashMap在JDK8版本的时候,底层数据结构是【数组+链表 + 红黑树】
3、JDK8HashMap设计的思想:因为链表过长会造成查询较慢,所以在JDK8中,链表长到一定程度,会
转化为红黑树,用于保持一定的效率
4、7和8版本的HashMap,链表都是单向链表,JDK7中HashMap的链表使用的是头插法,JDK8中
HashMap的链表使用尾插法
(二)HashMap的源码分析JDK7版本

	public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>,
	Cloneable, Serializable
	实现了Map接口,因为HashMap是Map的实现类
	实现了Cloneable接口,证明可以使用Object中的clone方法
	实现了Serializable接口,证明支持序列化
	/**
	* 默认初始化容量为16
	*/
	static final int DEFAULT_INITIAL_CAPACITY = 16;
	/**
	* 最大容量是2^30
	*/
	static final int MAXIMUM_CAPACITY = 1 << 30;
	/**
	* 加载因子
	*/
	static final float DEFAULT_LOAD_FACTOR = 0.75f;
	/**
	* 用于存储简直对对象的数组
	*/
	transient Entry<K,V>[] table;
	/**
	* 键值对对象个数
	*/
	transient int size;
	/**
	* 扩容阈值:当前数组容量*加载因子
	* 作用:当我们数组快要装满的时候,达到了阈值,数组就会扩容
	* 扩容阈值:默认的扩容阈值为12
	*/
	int threshold;
	/**
	* 加载因子
	*/
	final float loadFactor;
	public HashMap() {
		//将默认初始化容量16,和默认加载因子0.75,通过this语句访问有参构造并赋值
		this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
	}
	public HashMap(int initialCapacity, float loadFactor) {
		//判断指定容量如果小于0,抛出非法参数异常,不予创建对象
		if (initialCapacity < 0)
			throw new IllegalArgumentException("Illegal initial capacity: " +
												initialCapacity);
		//当指定的容量极大时,创建对象时只能将HashMap能够提供的最大容量作为当前Map集合底层容量
		if (initialCapacity > MAXIMUM_CAPACITY)
			initialCapacity = MAXIMUM_CAPACITY;
		//判断加载因子如果小于0或者不是个数就抛出非法参数异常,不予创建对象
		if (loadFactor <= 0 || Float.isNaN(loadFactor))
			throw new IllegalArgumentException("Illegal load factor: " +
												loadFactor);
		// Find a power of 2 >= initialCapacity
		int capacity = 1;
		// 确保容积最小为16
		while (capacity < initialCapacity)
			capacity <<= 1;
		//将传入的加载因子,赋值给当前正在创建HashMap对象
		this.loadFactor = loadFactor;
		//计算扩容阈值
		threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
		//创建一个容量为16的Entry数组
		table = new Entry[capacity];
		useAltHashing = sun.misc.VM.isBooted() &&
				(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
		init();
	}
	/**
	* 内部类,它的对象就是我们的键值对对象,也就是Entry对象
	*/
	static class Entry<K,V> implements Map.Entry<K,V> {
		//键值对对象所包含的成员变量
		final K key;//键
		V value;//值
		Entry<K,V> next;//用于将来形成链表挂载时,用于指向下一个链表节点的地址
		int hash;//当前对象的哈希值
		/**
		* Creates new entry.
		*/
		Entry(int h, K k, V v, Entry<K,V> n) {
			value = v;
			next = n;
			key = k;
			hash = h;
		}
		public final K getKey() {
			return key;
		}
		public final V getValue() {
			return value;
		}
		public final V setValue(V newValue) {
			V oldValue = value;//将旧的值先存起来
			value = newValue;//将新的值把旧的值替换
			return oldValue;//返回旧的值
		}
		public final boolean equals(Object o) {
			if (!(o instanceof Map.Entry))
				return false;
			Map.Entry e = (Map.Entry)o;
			Object k1 = getKey();
			Object k2 = e.getKey();
			if (k1 == k2 || (k1 != null && k1.equals(k2))) {
				Object v1 = getValue();
				Object v2 = e.getValue();
				if (v1 == v2 || (v1 != null && v1.equals(v2)))
				return true;
			}
			return false;
		}
		public final int hashCode() {
			return (key==null ? 0 : key.hashCode()) ^ (value==null ? 0 : value.hashCode());
					
		}
		public final String toString() {
			return getKey() + "=" + getValue();
			}
	}
	public V put(K key, V value) {
		//判断当前的key值如果为null,就做出添加null键的操作,也就是HashMap允许添加null键
		if (key == null)
			return putForNullKey(value);
		//计算当前键的哈希值
		int hash = hash(key);
		//索引:计算将来key要添加到数组的什么位置
		int i = indexFor(hash, table.length);
		//将当前计算出的索引上的元素拿过来
		//判断当前索引i上是否已经存了元素
		//如果已经存了元素就进入循环进一步判断
		//for循环是根据已经有的键修改对应的值
		// 将当前索引上的元素地址存储 地址不为空进入循环
		for (Entry<K,V> e = table[i]; e != null; e = e.next) {
			Object k;
			//比较当前老元素哈希和新的哈希一样吗?如果不一样,就不是一个键,如果一样,再通过equals确认,确保是一个键
			if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
			V oldValue = e.value;
			e.value = value;//修改原来的value为新的value
			e.recordAccess(this);
			return oldValue;
			}
		}
		modCount++;
		addEntry(hash, key, value, i);
		return null;
	}
	void addEntry(int hash, K key, V value, int bucketIndex) {
		//判断如果现在数组容量已经达到了扩容的阈值,并且当前元素要插入的位置还有元素,就进行扩容
		if ((size >= threshold) && (null != table[bucketIndex])) {
				//扩容为原来的2倍
				resize(2 * table.length);
				hash = (null != key) ? hash(key) : 0;
				bucketIndex = indexFor(hash, table.length);
		}
		createEntry(hash, key, value, bucketIndex);
	}
	void createEntry(int hash, K key, V value, int bucketIndex) {
		//将数组的老元素的地址值临时存储
		Entry<K,V> e = table[bucketIndex];
		//将新元素添加到数组的指定位置,并且新元素指向老元素,按照头插法形成链表
		table[bucketIndex] = new Entry<>(hash, key, value, e);
		size++;
	}

总结:
(1)JDK7中,创建一个HashMap对象,底层使用【数组+单向链表】的哈希表来存储元素
(2)JDK7的数组初始容量默认为16,加载因子是0.75,扩容阈值是12
(3)数组:用于存放内部类Entry对象的,Entry对象实际上就是键值对对象
(4)加载因子:用于根据当前的数组大小,计算数组填满多少个元素之后就可以扩容了,设定为0.75是
为了保证较好的数组空间的利用,和及时的扩容
(5)当数组中的元素个数达到12个时(基于数组容量为16的情况),不直接扩容,我们添加一个元素
进数组,如果要添加的位置上已经有了元素,则数组扩容为【原来的容量乘2】,如果要添加的位置上没
有元素,则不扩容直接添加
(6)当添加一个元素的时候,不需要扩容,则创建Entry对象添加,并且,如果目标位置没有元素,直
接添加,如果目标位置有元素,则使用【头插法】行程单向链表
(7)当put方法添加key和value的时候,如果key的hash和具体的值都和某个已存在的元素的key相
等,就用新的value替换旧的value
(三)HashMap的源码分析JDK8版本
1、JDK8相较于JDK7底层从【数组+链表】变成了【数组+链表+红黑树】,主要目的在于提升效率,因
为如果链表挂载过长,会降低查询效率,如果链表很长的时候,将链表转为红黑树,这样一来可以保证
较高的查询效率

/**
* 默认初始化容量为16
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 最大容量为2^30
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认加载因子:0.75
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 树形结构阈值:当链表长度为8的时候,就从链表转为红黑树(条件一)
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 回退链表结构阈值:当红黑树节点为6时,从红黑树变成链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 当数组的长度达到64的时候,可以将链表转为红黑树(条件二)
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 底层数组
*/
transient Node<K,V>[] table;
/**
* 键值对对象的个数
*/
transient int size;
/**
* 数组扩容阈值
* @serial
*/
int threshold;
/**
* 加载因子
* @serial
*/
final float loadFactor;
//当使用空参构造创建对象时,仅仅是准备了默认的加载因子0.75,【并没有准备数组】
public HashMap() {
	this.loadFactor = DEFAULT_LOAD_FACTOR;
}
//调用put方法,重算key的哈希值,并且带上key和value
public V put(K key, V value) {
	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;
	if ((tab = table) == null || (n = tab.length) == 0)
	//当数组没有创建的时候,创建一个长度为16的数组并且返回,赋值给tab
		n = (tab = resize()).length;//n = 16
	//经过一套算法,得到一个数组的索引i,并且从数组中获取i对应的元素,判断是否为空
	if ((p = tab[i = (n - 1) & hash]) == null)
		//调用newNode方法,将传入的key和value创建成一个键值对对象并且添加到数组中
		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;//当现在要添加的key和老元素的key一样,先临时把老元素的key存起来
	else if (p instanceof TreeNode)//判断当前获取到的老键值对对象是不是红黑树的节点
		e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
	else {
		/*
		* 当我们要插入key,value时,但是数组选定的位置已经有了元素
		* 就获取这个元素,看看这个元素有没有下一个节点
		* 如果没有下一个链表节点,就利用尾插发形成单向链表挂在
		* 如果有下一个链表节点,就通过循环,挪到下一个链表节点再判断这个节点有没有下一个节点,如果没有就挂  载,如果有继续向下移动
		*
		* 当移动数量达到8时,就尝试将链表转红黑树,但是还需要判断,数组容量是否足
		够64
		*
		* */
		for (int binCount = 0; ; ++binCount) {
			if ((e = p.next) == null) {
				p.next = newNode(hash, key, value, null);
				//当链表长度大于等于7,就到
				if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
					treeifyBin(tab, hash);
				break;
			}
			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;
}
//当数组不存在的时候用于创建数组,或者容量不够的时候用于扩容
final Node<K,V>[] resize() {
	Node<K,V>[] oldTab = table;//先将老数组的地址值存储起来:null
	int oldCap = (oldTab == null) ? 0 : oldTab.length;//老数组的长度0
	int oldThr = threshold;//老阈值:0
	int newCap, newThr = 0;
	/*
	* 数组必须存在且长度大于0
	* 才能进入这个if
	* */
	if (oldCap > 0) {
		if (oldCap >= MAXIMUM_CAPACITY) {
			threshold = Integer.MAX_VALUE;
			return oldTab;
		}
		//老数组扩容为原来的2倍,并且大于默认的16
		else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
			oldCap >= DEFAULT_INITIAL_CAPACITY)
		//原来的阈值扩大2倍
		newThr = oldThr << 1; // double threshold
	}
	else if (oldThr > 0) // initial capacity was placed in threshold
		newCap = oldThr;
	else { // zero initial threshold signifies using defaults
		//数组容量为16
		newCap = DEFAULT_INITIAL_CAPACITY;
		//扩容阈值大小为12
		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"})
	//创建键值对对象数组:16
	Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
	//将创建好的数组地址赋值给对象的数组声明
	//到此为止,对象才有一个容量为16的Node数组,数组扩容阈值为12,加载因子为0.75
	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);
				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;
						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;
}
/**
* 创建一个键值对对象并且返回
*/
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
	return new Node<>(hash, key, value, next);
}

总结:
过程一:从创建集合对象到添加第一个值
(1)使用空参构造创建HashMap的时候,底层仅仅是给集合对象赋值了默认的0.75的加载因子
(2)当我们调用put方法添加值的时候,帮助我们在底层创建一个容量为16的数组,并且赋值数组扩容
阈值为12
(3)将键值对对象Node创建出来,添加到数组中
过程二:
(1)调用put方法添加值
(2)判断要添加的这个目标位置上有没有元素存在,如果没有直接添加
(3)要添加的位置上有元素,判断键是否一样,如果一样,根据键修改值

(4)如果要添加位置上有元素,但是键不一样,就使用【尾插发】形成单向链表挂载
(5)如果在单向链表挂载的过程中,链表长度为8,尝试转为红黑树,但是,如果数组的长度也至少64,才能转换成功,否则失败
结论:JDK8的HashMap默认容量为16,默认加载因子为0.75,默认扩容阈值为12,当链表的长度大于8并且数组的长度大于64,才能将链表转为红黑树;当红黑树节点个数小于6,回退到链表。
注意:为什么链表转红黑树要求苛刻,因为转换树的过程,树实现自平衡的过程,都非常消耗内容,所
以不会轻易的将链表转为红黑树

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值