1. HashMap底层数据结构?
在JDK1.7之前,hashmap的底层数据结构是数组加链表的形式,里面存放着一个个Node节点,每个节点有对应的Key和Valu值,同时还存有一个Node类型的Next值用于存储写一个节点的地址值.
而在JDK1.8之后,HashMap中引入了红黑树.
如果当前hashmap的数组容量大于或者等于64,并且链表上的元素超过8时,就会进行树化
2. HashMap的初始容量是多少?
我们先想一个问题,我们都知道HashMap里面有一个数组,那么我们在创建一个HashMap对象的时候,它的容量是多少呢? 我们在使用HashMap无参构造器来创建一个HashMap对象的时候,会默认设置负载因子0.75,但不会立即分配底层的数组空间.
对于Java这样设计的目的,我猜想是因为哈希表是一个比较耗费内存的数据结构,java怕你只创建不赋值.
在我们第一次调用put方法时,该方法会先检查数组是否为空,如果为空则调用扩容方法resize()初始化数组,默认容量为16
3.hash扰动算法和寻址算法
因为每个对象的hashcode不同,但他们的hashcode通过扰动算法后,可能得到一样的hash;
在进行寻址操作时,hash = hashcode ^ hashcode>>>16(右移16位)通过这样一个扰动函数得到得到hash值,而使用这种方式的目的是为了让hashcode的高十六位一起参与运算,这样得到的hash值随机性更大,能减小哈希冲突的概率`
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
`}`
然后将Node对象的hash进行一个寻址算法 index = hash & (table.length -1) (table代表数组长度,默认是十六) ,在这时16-1=15 的二进制刚好是 1111 ,所以hash与其最终参与计算的也只有后四位 .
p = tab[i = (n - 1) & hash])//
而当table进行一次扩容后,长度来到32 ,此时32 -1 =31 的二进制数为 1 1111 ,这时hash进行寻址算法时会多运算一位,
因为扩容后要将原来哈希表的元素映射到新表中,所以原来在同一个桶的元素可能映射到不同位置,这种行为,被称为rehash
假设原来的桶在15的位置,进行扩容后,其中新高位为1的元素则添加到31位置,新高位为0的元素则留在原桶内
4 哈希冲突
首先哈希函数的输入空间是无限的,但是存储空间是有限的.这句话是什么意思呢? 意思就是,我们能得到的hash值是有非常多的,当把其对应的元素映射到HashMap中时,通过寻址算法后,很可能会映射到同一个桶内.
通俗来说,假设现在有四只鸽子,但只有三个鸽巢的话,那是不是必然会有一个鸽巢里住有两只鸽子呢?
那么哈希冲突说完后,我们来探讨一下怎么减少哈希冲突出现的概率.
一般来说哈希冲突无法避免,但我们可以通过设计更好的哈希函数,和扩容机制来降低其影响
5.哈希的扩容resize()
当我们创建了一个哈希表后,一般来说只有两种情况需要去进行hashmap的扩容.
第一种情况是HashMap未初始化时,会进行一次扩容.
如果是初次扩容且没有设定初始值,则会将数组容量设置为16,扩容阈值设为16*0.75 =12;
第二种情况则是当hashmap中,当前元素数量达到扩容阈值(数组容量*扩容因子),这时会进行扩容
扩容的大小是原来数组长度的两倍;(这里有一点需要提一下,若旧容量已达到 MAXIMUM_CAPACITY(2^30),则不再扩容,阈值设为 Integer.MAX_VALUE)
//情况二:
//旧容量>0,说明不是第一次扩容
if (oldCap > 0) {
//判断是否大于最大容量
if (oldCap >= MAXIMUM_CAPACITY) {
//大于最大容量则将扩容阈值设为Integer.MAX_VALUE`
threshold = Integer.MAX_VALUE;
//返回旧表
return oldTab;
}
//扩容为原来的两倍,新阈值newThr也设为旧阈值的两倍。
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
数组扩容完成之后,会将扩容阈值会更改, 新阈值 = 新容量 *0.75
在数组完成扩容之后,会生成一个新数组,这时需要把数组中的元素转移到新数组中(rehash) ,同时将旧数组进行回收
rehash
在进行再哈希操作时,会对哈希表中的各个不为空的桶进行判断,一共有以下几种情况
一:桶只有一个元素,即该Node对象的next值为null,则直接直接将该元素进行寻址操作放到新数组的桶中
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
二:桶中不只有一个元素,已经链化为一条链表,但还没有树化
这时会根据某种规则将链表分割为两个表(低位表和高位表),
低位表的位置和原来一样(原来在15位置,则在新数组中也是15位置)
高位表的位置则是 index = index +oldCap =15+16 =31 (即旧索引位置加旧数组容量)
而分割高位表低位表的规则呢则是,hash & oldCap(Node的hash&旧数组容量)
假设初始容量为16 其二进制位 --> 1 0000 如果元素的hash是 0 1111 则结果是 0 ,代表低位表,桶位不变
如果元素的hash是1 1111 ,这时结果是16 ,代表是高位表,映射的位置是newTab[j + oldCap] = hiHead;
在确定完rehash的位置后且链表不为空
这时就会对链表进行分割,从第一个Node对象开始,将node的next设置为null ,断开与链表的连接,然后放到新桶
三:已经树化,这时会进行调用split()进行分割
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
HashMap容量大小为什么要设置成2的倍数?
因为当数组长度(n)是2的倍数的时候,就可以直接通过逻辑与运算(&)计算下标位置,比取模速度更快。
6.put
在hashmap执行put方法进行添加元素操作时,首先会进行一次判断,判断数组是否为空,如果为空,说明是刚创建的哈希表,这时就会去调用resize方法进行初次扩容
如果不为空,则会通过哈希函数计算出当前key的hash然后通过寻址算法找到数组中对应的桶的位置.然后进行判断:
接下来有四种情况
第一种情况:
首先会判断当前这个桶是否为null,如果为null则直接将元素添加,next设为null
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
第二种情况
如果不为null,则与当前桶中的这个Node节点,通过比较hash和key的内容来判断是否为同一元素,如果hash和key都相同则会替换元素.(在JDK8之后使用的是尾插法)
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);
第四种情况
说明当前桶已经链化,但还没有树化为红黑树,接下来会遍历这个链表,来寻找是否有hash和key都相同的元素,如果有就替换
如果没有,则遍历到元素的next为null的节点,这时说明已经是链表的最后一个node节点,将元素存放到链表的最后面
这时会再次进行判断,如果当前链表的长度达到8且当前数组的长度大于等于64时,就会树化这条链表
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;
}
//只要有重复元素就替换
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
在添加完元素后判断是否需要扩容
putVal 流程:
- 检查数组是否为空 → 是则调用 resize() 初始化
- 计算哈希值和桶索引
- 处理桶:
- a. 桶为空 → 直接插入新节点**
- b. 桶不为空:**
- i. 比较 hash 和 key → 相同则替换**
- ii. 桶是红黑树 → 调用 putTreeVal() 插入树**
- iii. 桶是链表:**
- *遍历链表,查找或插入节点
-插入后检查链表长度:
- *≥8 且数组容量 ≥64 → 树化
- *否则 → 保持链表
- *遍历链表,查找或插入节点
-插入后检查链表长度:
- 检查是否需要扩容 → 是则调用 resize()