HashMap底层原理 (二) put和resize方法
HashMap的put方法
hashMap中最常用的put方法
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) {
....
}
可以从上面的代码看出put在调用的计算hash方法以后又调用了putVal,可以确认putVal是HashMap中put的核心方法
现在对上面代码逐句分析
//putVal 变量
Node<K,V>[] tab; //数组
Node<K,V> p; //元素
int n, i; // n数组长度 i数组下标
//源码
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//转换后
tab = table;
n = tab.length;
if(tab==null||n==0){
tab = ressize(); //数组扩容的核心代码.稍后分析...
n = tab.length;
}
//上面代码表示 如果当前的HashMap在刚开始table未定义的情况下 生成一个table,
//在new HashMap()的时候结只确定了扩容因子,在put的时候确认hashmap会使用的情况下才会开辟内存创建数组
//转换代码==> i=(n-1)&hash; p=tab[i]; if(p==null) 判断在该桶位上知否有元素
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); //没有就创建一个node直接放在该桶位上
else {//该位置上有元素的情况
Node<K,V> e; K k;
//第一种情况 当桶位上有元素,且桶位上元素的key值和传进来的key值刚好相等时
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;//将桶位上的元素p直接赋值给临时元素e
else if (p instanceof TreeNode) //第二种情况 如果p是一个树节点,那证明该桶位存储的是红黑树
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //
else {//最后一种情况 桶位上有元素且是一个链表的情况
for (int binCount = 0; ; ++binCount) {
//将p指向的下一个元素赋值给e 后判断e是否为空
if ((e = p.next) == null) {//如果为空 即p是链表最后一个元素
p.next = newNode(hash, key, value, null);//创建一个新的节点 并让p的下一个指向新节点
//binCount代表当前链表的长度 当链表长度大于等于设置的转换树形参数时,链表转换成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);//链表转换成红黑树
//完成以后跳出循环
break;
}
//如果当前hash 和 key都相等的情况下 证明e就是我们需要替换值的元素 直接跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//当e不为空的时候证明 该元素的key是在列表中存在的,我们就只需要替换他的value
if (e != null) { // existing mapping for key
V oldValue = e.value; //获得以前的value值
//onlyIfAbsent 如果当前位置已存在一个值,是否替换,false是替换,true是不替换
if (!onlyIfAbsent || oldValue == null)
e.value = value; //如果满足替换条件 就替换value
afterNodeAccess(e);
return oldValue;
}
}
HashMap的resize方法
hashMap中的扩容机制
话不多少直接开始看源码
//创建一个数组来存放旧的table
Node<K,V>[] oldTab = table;
//获取旧数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//获取扩容阈值
int oldThr = threshold;
//定义新的数组长度和扩容阈值
int newCap, newThr = 0;
上面的几个变量用来收集旧数组的信息
if (oldCap > 0) {//当旧数组的长度大于0时
if (oldCap >= MAXIMUM_CAPACITY) { //判断旧数组的长度是否超过定义的最大数组长度 如果超过
threshold = Integer.MAX_VALUE;//设置长度为最大长度
return oldTab;//返回旧数组 因为已经超过了定义的最大长度,hashMap将不再数组扩容
}
//因为在位运算中 整体向左移动一位 就相当于当前数字*2 即新组数赋值为就数组长度的2倍,也就是2倍扩容
//如果扩容两倍后小于定义的数组最大长度 且 旧的长度>=16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//下一次的扩容阈值在旧的扩容阈值基础上*2
newThr = oldThr << 1; // double threshold
}
//因为上面已经确定了oldCap是要大于0的 所以如果要执行后面两个 那么oldCap也就只能等于0 也就是说是新数组
//如果设置的扩容阈值大于0 要始oldCap=0且oldThr>0 需要调用可以设置的构造函数 如:HashMap(int initialCapacity, float loadFactor)
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;//新的扩容阈值等于旧的扩容阈值
// 相当于oldCap=0且oldThr也等于0时调用 比如 new HashMap()会就是oldCap=0且oldThr=0
else { // zero initial threshold signifies using defaults
//新的数组长度为默认的长度 16
newCap = DEFAULT_INITIAL_CAPACITY;
//新的阈值0.75*16 =12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果新的阈值等于0
if (newThr == 0) {
//新的长度 * 设置的扩容因子
float ft = (float)newCap * loadFactor;
//判断是否超过最大值 如果超过最大值则设置为Integer的最大值,否者设置为取整后的阈值
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];
table = newTab;
//如果旧的数组不为空
if (oldTab != null) {
//循环遍历就数组中的所有node并插入新的数组中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//当数组中的元素不为空时
if ((e = oldTab[j]) != null) {
//将旧数组改位置上的node清空
oldTab[j] = null;
//如果下一位没有元素则证明该位置上面只有这一个元素
if (e.next == null)
//重新计算node的位置并设置
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)//当元素为树时
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//定义低位链 loHead 低位链表最后一个元素loTail
Node<K,V> loHead = null, loTail = null;
//定义高位链hiHead 高位链表最后一个元素hiTail
Node<K,V> hiHead = null, hiTail = null;
//这里为什么要拆分为高位链和低位链 前面知道数组扩容是2倍 也就是说数组长度左移1位
//根据寻址算法 (n-1)&hash 那么 如果扩容前的数组长度为16 扩容后为32
//根据二进制计算 16-1=15 0000 0000 0000 0000 0000 0000 0000 1111
//扩容后 32-1=31 0000 0000 0000 0000 0000 0000 0001 1111
//而我们的hash值 在做&运算时 前面0的部分都一样 后面4个1的部分也都相同
//那么唯一有区别的地方就在于扩容后的最高位数 有0和1的区别
//而最高位的长度差值又正好是原数组的长度,所以 同一个桶位中的链表拆分只能在这两个之桶位之间存放
//那么计算出来的低位链表存放在原位置,而高位链表则存放在原位置+扩容长度(原数组长度)的桶位上
//再回到源码
Node<K,V> next;
do {
//e.的下一个元素赋值个next
next = e.next;
//e.hash & oldCap 如果为0则像上述所说存在低位链表中
if ((e.hash & oldCap) == 0) {
if (loTail == null)//如果低位链表最后一个没有数据
loHead = e; //则放到第一个
else
loTail.next = e; //否者放到下一个
loTail = e; //将最后一个设置为当前元素
}
else { //如果不为0那么只能为1 放到高位链表中
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;