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;
}