hashmap

HashMap底层原理

目录

1、hashmap的存储结构
2、基本变量说明
3、构造函数
4、键值对的放入
5、hash计算
6、扩容

正文

1、hashmap的存储结构

1.7之前hashmap使用的是数组加链表的结构进行数据存储的,1.8之后新增了红黑树。

红黑树的简单特点:一句话概括(红黑树是一颗平衡的二叉搜索树)

基本定义: 1、每个节点都是红色或者是黑色

​ 2、跟节点是黑色

​ 3、每个叶子节点(实际上有的是null指针)都是黑色的

​ 4、不能有两个相邻的红色节点

​ 5、对于每个节点,从该节点到其所有子孙节点的路劲中包含的黑色节点数必须相同

使用链表查询的时间复杂度是O(n^2) 红黑树的查询时间是O(logn)

hashmap结构示意图:

[外链图片转存失败,源站可能在这里插入图片描述
有防盗链机制,建议将图片保存下来直接上传(img-tsdqUj48-1628930807126)(D:\mycsdn\images\image-20210814162006809.png)]

基本的变量

以下的内容是源码中获取的,有兴趣的小伙伴也可以自己去查看

//默认初始容量为16,必须是2的n次幂
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;
构造函数

hashmap提供了4个构造函数供我们使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TsHL26hX-1628930807131)(D:\mycsdn\images\image-20210814163022292.png)]在这里插入图片描述

键值对的放入

我们可以简单地理解存放键值对的步骤为

1、先将键值对的key进行hash计算得到hash值

2、将这个hash值找数组的下标,判断 当前下标是否有元素,如果没有元素直接添加

2.1如果有元素:当前元素位置的hash等于传过来的元素的hash并且两者的key相同,说明发生了碰撞,转到①执行

2.2如果当前是红黑树结构,就把他加入红黑树中

2.3是普通链表,则直接采用尾插法,将节点加入到链表尾部

①会进行一个覆盖的操作,节点的位置不变,但是值被替换

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

这里会先判断key是否为空,若是则返回0.这也说明了hashmap是支持key为空的。

扩容(resize)
final Node<K,V>[] resize() {
	//旧数组
	Node<K,V>[] oldTab = table;
	//旧数组的容量
	int oldCap = (oldTab == null) ? 0 : oldTab.length;
	//旧数组的扩容阈值,注意看,这里取的是当前对象的 threshold 值,下边的第2种情况会用到。
	int oldThr = threshold;
	//初始化新数组的容量和阈值,分三种情况讨论。
	int newCap, newThr = 0;
	//1.当旧数组的容量大于0时,说明在这之前肯定调用过 resize扩容过一次,才会导致旧容量不为0。
	//为什么这样说呢,之前我在 tableSizeFor 卖了个关子,需要注意的是,它返回的值是赋给了 threshold 而不是 capacity。
	//我们在这之前,压根就没有在任何地方看到过,它给 capacity 赋初始值。
	if (oldCap > 0) {
		//容量达到了最大值
		if (oldCap >= MAXIMUM_CAPACITY) {
			threshold = Integer.MAX_VALUE;
			return oldTab;
		}
		//新数组的容量和阈值都扩大原来的2倍
		else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
				 oldCap >= DEFAULT_INITIAL_CAPACITY)
			newThr = oldThr << 1; // double threshold
	}
	//2.到这里,说明 oldCap <= 0,并且 oldThr(threshold) > 0,这就是 map 初始化的时候,第一次调用 resize的情况
	//而 oldThr的值等于 threshold,此时的 threshold 是通过 tableSizeFor 方法得到的一个2的n次幂的值(我们以16为例)。
	//因此,需要把 oldThr 的值,也就是 threshold ,赋值给新数组的容量 newCap,以保证数组的容量是2的n次幂。
	//所以我们可以得出结论,当map第一次 put 元素的时候,就会走到这个分支,把数组的容量设置为正确的值(2的n次幂)
	//但是,此时 threshold 的值也是2的n次幂,这不对啊,它应该是数组的容量乘以加载因子才对。别着急,这个会在③处理。
	else if (oldThr > 0) // initial capacity was placed in threshold
		newCap = oldThr;
	//3.到这里,说明 oldCap 和 oldThr 都是小于等于0的。也说明我们的map是通过默认无参构造来创建的,
	//于是,数组的容量和阈值都取默认值就可以了,即 16 和 12。
	else {               // zero initial threshold signifies using defaults
		newCap = DEFAULT_INITIAL_CAPACITY;
		newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
	}
	//③ 这里就是处理第2种情况,因为只有这种情况 newThr 才为0,
	//因此计算 newThr(用 newCap即16 乘以加载因子 0.75,得到 12) ,并把它赋值给 threshold
	if (newThr == 0) {
		float ft = (float)newCap * loadFactor;
		newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
				  (int)ft : Integer.MAX_VALUE);
	}
	//赋予 threshold 正确的值,表示数组下次需要扩容的阈值(此时就把原来的 16 修正为了 12)。
	threshold = newThr;
	@SuppressWarnings({"rawtypes","unchecked"})
		//我们可以发现,在构造函数时,并没有创建数组,在第一次调用put方法,导致resize的时候,才会把数组创建出来。这是为了延迟加载,提高效率。
		Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
	table = newTab;
	//如果原来的数组不为空,那么我们就需要把原来数组中的元素重新分配到新的数组中
	//如果是第2种情况,由于是第一次调用resize,此时数组肯定是空的,因此也就不需要重新分配元素。
	if (oldTab != null) {
		//遍历旧数组
		for (int j = 0; j < oldCap; ++j) {
			Node<K,V> e;
			//取到当前下标的第一个元素,如果存在,则分三种情况重新分配位置
			if ((e = oldTab[j]) != null) {
				oldTab[j] = null;
				//1.如果当前元素的下一个元素为空,则说明此处只有一个元素
				//则直接用它的hash()值和新数组的容量取模就可以了,得到新的下标位置。
				if (e.next == null)
					newTab[e.hash & (newCap - 1)] = e;
				//2.如果是红黑树结构,则拆分红黑树,必要时有可能退化为链表
				else if (e instanceof TreeNode)
					((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
				//3.到这里说明,这是一个长度大于 1 的普通链表,则需要计算并
				//判断当前位置的链表是否需要移动到新的位置
				else { // preserve order
					// loHead 和 loTail 分别代表链表旧位置的头尾节点
					Node<K,V> loHead = null, loTail = null;
					// hiHead 和 hiTail 分别代表链表移动到新位置的头尾节点
					Node<K,V> hiHead = null, hiTail = null;
					Node<K,V> next;
					do {
						next = e.next;
						//如果当前元素的hash值和oldCap做与运算为0,则原位置不变
						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;
}




评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值