HashMap数据结构
数组+链表 1.7
数组+链表+红黑树 1.8
HashMap为什么要用链表?
解决hash冲突
为什么要用红黑树?(查找接近二分查找)
方便查询(时间复杂度低)
HashMap的容量为什么要为2的次方数呢?
1.hash运算方便(在进行hash运算的时候可以通过位运算来进行,不必进行取模,提高效率)
2.在数组在扩容的时候,方便计算新的位置
hash函数
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这里的hash函数和1.7的不同,因为1.8新增了红黑树这个结构,所以不用像1.7的那么复杂.
这种做法就是将hashCode()返回的hash值异或上该值右移16位.就是让高位参加运算,增加散列性.
这样hash的好处还有一点:
在数组进行扩容的时候, 旧数组table[i]位置的所有值在转移到新数组的位置只会有两种情况:
1.在新数组的 newTable[i]位置
2.在新数组的 newTable[i+oldCap] 位置
为什么会这样呢?
举个例子:
有两个hashCode : e.h1 = 1010 1001 e.h2 = 1101 1001 (低4位相同,高位不同)
数组长度: table.length = 16
该值在table的存储位置为 e.h ^ (16 -1)
计算e.h1:
1 0 1 0 1 0 0 1 e.h1
& 0 0 0 0 1 1 1 1 15
= 0 0 0 0 1 0 0 1 = 9 所以放入table[9]
此时数组扩容 table.length = 2 * 16 =32
再次计算: e.h1 ^ (32 -1)
1 0 1 0 1 0 0 1 e.h1
& 0 0 0 1 1 1 1 1 31
= 0 0 0 0 1 0 0 1 = 9 所以放入newTable[9] table[i]的数放入 newTable[i]
计算e.h2:
1 1 0 1 1 0 0 1 e.h2
& 0 0 0 0 1 1 1 1 15
= 0 0 0 0 1 0 0 1 = 9 所以放入table[9]
此时数组扩容 table.length = 2 * 16 =32
再次计算: e.h2 ^ (32 -1)
1 1 0 1 1 0 0 1 e.h2
& 0 0 0 1 1 1 1 1 31
= 0 0 0 1 1 0 0 1 = 25 所以放入newTable[25] table[i]的数放入newTable[i+oldCap]
发现再扩容之后的低四位运算不变,只是多了一位参与运算,而这一位计算的结果只有0或1, 0代表index运算结果和原先结果一样,1则代表增加了 多出来计算的那一位的值,由例子来看,在计算中新增的那一位是 16 ,所以index+新增的那一位的值即可,也可以看做是增加了原先的数组长度.
JDK 1.8 HashMap分析
属性:
//初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当链表的大小等于或大于8,就会变成树结构 (变树的条件之一)
static final int TREEIFY_THRESHOLD = 8;
//当树的大小小于或等于6,就会变成链表结构
static final int UNTREEIFY_THRESHOLD = 6;
//最小树形化容量阈值为,当表的长度(数组的长度)>该值,才允许将链表树化
//否则,桶内元素过多时直接扩容,而不是树形化
//了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64; (table.length>this : 是变树的第二个条件)
//主要数据结构
transient Node<K,V>[] table;
//Node节点
class Node<K,V> implement Map.Entry{
int hash;
K key;
V value;
Node<K,V> next;
}
/**TreeNode继承了LinkedHashMap.Entry ,
而Entry又继承了HashMap.Node,所以他会有HashMap的Node的属性next,
自己也有prev属性,所以在是一颗树的同时也维护了一个双向链表.
**/
TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
}
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
构造函数
public HashMap(int initialCapacity, float loadFactor) {
//如果给定容量小于0,则不合法,抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//如果给定容量大于最大容量2<<30, 则初始化容量为最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//判断加载因子的合法性
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//赋值给HashMap的属性
this.loadFactor = loadFactor;
//这里需要对对传入的容量大小进行转化,会转为大于或等于该值的最小2次方数
//例: initialCapacity = 10, 进行tableSizeFor()方法会变为 16 因为 10 < 2^4=16
this.threshold = tableSizeFor(initialCapacity);
}
//该方法的目的就是将传入的数转为大于或等于该值的最小2次方数
static final int tableSizeFor(int cap) {
//先将该值-1
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
以二进制的形式进行运算
例:i=18
18 = 0001 0010
(1) 0001 0010右移一位: 0000 1001
0001 0010 或 0000 1001 : 0001 1011
(2) 0001 1011 右移2位: 0000 0110
0001 1011 或 0000 0110 :0001 1111
(3)0001 1111 右移4位: 0000 0001
0001 1111 或 0000 0000 : 0001 1111
(4)0001 1111 右移8位:0000 0000
0001 1111 或 0000 0000 :0001 1111
(5)0001 1111 右移16位:0000 0000
0001 1111 或 0000 0000 :0001 1111
此时结果为 0001 1111 也就是31 最后进行加1也就会变成 32.因为 18< (2^6=32)
右移与或运算的目的就是想让某个数字的低位都变为1,再用该结果 加1 ,即得到了想要的结果。
问题:HashMap是在你new 的是否就已经创建(为数组分配空间)好了吗?
答案:不是这样的,在分析源码的时候,HashMap的构造函数上面没有对table属性进行操作.那是在什么时候进行初始化table的呢.
不妨可以调试一下
HashMap<String,String> map = new HashMap();
map.put("1","2");
我们可以点进去put方法里面看一下put的过程:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
他首先调用了putVal()方法.
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//创建一个tab数组,一个p引用,两个变量
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//判断table[i]位置的key是否相等,如果相等,将节点e指向该位置
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);
//如果不是以上情况,那么只能是一个链表了
else {
//定义一个变量,用来记录遍历的节点个数
for (int binCount = 0; ; ++binCount) {
//如果p.next不为null , 不进入第一个if
if ((e = p.next) == null) {
//如果p.next==null ,说明遍历到了最后一个节点
//创建一个新节点,进行尾插法
p.next = newNode(hash, key, value, null);
//需要判断bigCount是否大于7(链表长度过长需要进行树化,而树化的要求是链表的长度>8)
//因为bigCount是从0开始的.
//这里需要思考一个问题:是插入第8个元素的时候会树化,还是插入第9个元素时树化?
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//判断p.next的key是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//相当于 p = p.next(因为e = p.next)
p = e;
}
}
//判断e是否存在
if (e != null) { // existing mapping for key
//如果存在,获取旧值
V oldValue = e.value;
//判断是否需要更新该值.putVal()的参数onlyIfAbsent为false,所以需要更新该值
//key相同,value值需要进行覆盖操作
//可以在使用map.putIfAbsent("1","2");方法,表示如果key相同,不进行任何更新覆盖操作.
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); //子类用到的方法
//返回旧值
return oldValue;
}
}
//添加数据成功,版本号+1
++modCount;
//判断新的size是否大于扩容阈值,如果大于则需要进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict); //子类用到的方法
return null;
}
可以看出put的核心代码在这里.来分析一下.
- 在put的时候,第一件事就是去检查这个table是否为null,因为构造函数没有对table进行初始化,所以第一次调用put,table==null ,在这个时候,调用resize()方法,才进行初始化table;
- 数组初始化完成之后,通过hash & (table.length - 1)找出要插入的数组下标(相当于1.7的indexFor(hash,table.length)),找到位置后,判断当前位置是否为null,第一次插入肯定为null,所以直接new一个节点放到table[i]位置即可,不进入else ,modcount++(对HashMap的修改次数,也叫版本号)
- 进行size加一操作,然后判断size(数组存放的所有数据,包括链表存放的)是否大于阈值threshold.如果成立,则需要进行resize()扩容.(在上面初始化的时候也用到了resize(),该方法可用于初始化,同样也可用于扩容).
上面有一个问题:就是在插入第8个元素的时候会树化,还是插入第9个元素时树化?
for (int binCount = 0; ; ++binCount) {
//如果p.next不为null , 不进入第一个if
if ((e = p.next) == null) {
//如果p.next==null ,说明遍历到了最后一个节点
//创建一个新节点,进行尾插法
p.next = newNode(hash, key, value, null);
//需要判断bigCount是否大于7(链表长度过长需要进行树化,而树化的要求是链表的长度>8)
//因为bigCount是从0开始的.
//这里需要思考一个问题:是插入第8个元素的时候会树化,还是插入第9个元素时树化?
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//判断p.next的key是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//相当于 p = p.next(因为e = p.next)
p = e;
}
可以举例分析一下,因为bigCount是从0开始的,所以当我数组中有7个节点的时候,遍历完成之后++bigCount最终 = 6,接着进入if(p.next == null) ,创建新节点,插入第8个节点:p.next = newNode; 此时会去判断bigCount是否 >= 7,在这个时候,因为if()语句还没执行完,bigCount = 6 ,而现在链表的节点个数为8,因为刚刚新插入了一个节点.bigCount < 7不满足树化条件.可以看出,当插入第8个节点(元素)的时候,并没有进入if()语句,也就不会树化.
继续分析,此时链表有8个节点,最终++bigCount = 7,接着将第九个节点插入,此时判断bigCoung是否 >= 7,7 = 7 条件成立!
所以得出结论,在插入第9个节点的时候才会进行树化,也就是说当链表长度>8的时候,会进行链表树化(当然也不是绝对的,链表长度 > 8只是树化条件之一)
在putVal()方法里面有一个判断是判断bigCount >= 7(判断是否需要进行树化)
如果条件满足,来看一下方法 treeifyBin(Node<K,V>[] tab, int hash)
final void treeifyBin(Node<K,V>[] tab, int hash) {
//定义几个变量
int n, index; Node<K,V> e;
//这里是关键*************
//1.首先会判断数组是否为空,会返回false ,因为是 || 操作,所以前面为false还需要判断后面
//2.判断数组的长度是否小于MIN_TREEIFY_CAPACITY该值. 该值为64
//所以是否需要树化有两个条件
// 1.当前table[i]位置链表长度是否大于8
// 2.table的长度是否大于 64.
//两者需要同时满足的情况下,才会进行链表树化,否则只会进行数组的扩容resize()
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
//接下来要做的不是链表树化,而是将该table[i]下的链表组成一个双向链表
//定义一个TreeNode头结点 hd, 和辅助节点 t1
TreeNode<K,V> hd = null, tl = null;
do {
//replacementTreeNode()该方法返回一个TreeNode节点
TreeNode<K,V> p = replacementTreeNode(e, null);
//第一步将头结点指向Node链表的第一个节点
if (tl == null)
hd = p;
else {
//p相当于遍历Node链表的节点
//将链表的节点插入到该双向链表TreeNode的后面
p.prev = tl;
tl.next = p;
}
//辅助t1指向p
tl = p;
} while ((e = e.next) != null);
//将该双向链表放入table[i]位置,如果不为空,进行树化操作
if ((tab[index] = hd) != null)
//真正的链表树化操作
hd.treeify(tab);
}
}
从这个do{}while操作可以发现,它将原先的Node链表变成了一个双向的TreeNode链表.
也就是说后面树化操作形成的树 也是一个双向链表.
注意:链表的树化是为了方便查询,而数组的扩容,会让数据更加散列化,效果是一样的.
来看一下resize()方法到底是怎么实现的?
- resize()初始化流程:
final Node<K,V>[] resize() {
//首先创建一个数组指向原数组
Node<K,V>[] oldTab = table;
//判断原数组是否为null(这里在第一次put的时候数组还未初始化,所以为null,接下来进行初始化)
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//获取原来的扩容阈值
int oldThr = threshold;
//定义两个变量
int newCap, newThr = 0;
//判断原数组的长度是否大于 0 (扩容的时候会进入该if)
if (oldCap > 0) {
//判断数组旧长度是否大于最大容量
if (oldCap >= MAXIMUM_CAPACITY) {
//大于最大容量,则将该值赋值给扩容阈值
threshold = Integer.MAX_VALUE;
//直接返回旧长度,不能再扩容了
return oldTab;
}
//旧长度小于最大容量
//计算新容量newCap,odlCap<<1 也就是旧数组长度的2倍.
//判断新数组的长度是否小于最大容量,如果成立
//接着判断旧数组的大小是否大于默认初始容量,也就是16
//如果成立,则设置新数组的扩容阈值为旧数组的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//旧数组长度<=0 ,判断旧的扩容阈值是否大于0
else if (oldThr > 0) // initial capacity was placed in threshold
//将新值设置为旧值
newCap = oldThr;
//初始化操作
else { // zero initial threshold signifies using defaults
//设置新的容量为默认容量 16
newCap = DEFAULT_INITIAL_CAPACITY;
//计算新的扩容阈值
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"})
//初始化新数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//将新数组赋值给原来的数组
table = newTab;
//以上是初始化工作
//底下的都是关于扩容操作
..........
}
- resize()扩容:
核心代码
final Node<K,V>[] resize() {
//首先创建一个数组指向原数组
Node<K,V>[] oldTab = table;
//判断原数组是否为null(这里在第一次put的时候数组还未初始化,所以为null,接下来进行初始化)
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//获取原来的扩容阈值
int oldThr = threshold;
//定义两个变量
int newCap, newThr = 0;
//判断原数组的长度是否大于 0 (扩容的时候会进入该if)
if (oldCap > 0) {
//判断数组旧长度是否大于最大容量
if (oldCap >= MAXIMUM_CAPACITY) {
//大于最大容量,则将该值赋值给扩容阈值
threshold = Integer.MAX_VALUE;
//直接返回旧长度,不能再扩容了
return oldTab;
}
//旧长度小于最大容量
//计算新容量newCap,odlCap<<1 也就是旧数组长度的2倍.
//判断新数组的长度是否小于最大容量,如果成立
//接着判断旧数组的大小是否大于默认初始容量,也就是16
//如果成立,则设置新数组的扩容阈值为旧数组的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
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"})
//创建新的数组,大小为2*OldCap
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//原数组指向新数组
table = newTab;
//进行数据的转移操作
if (oldTab != null) {
//遍历原数组
for (int j = 0; j < oldCap; ++j) {
//定义一个辅助节点
Node<K,V> e;
//遍历每个table[i]位置的数据
if ((e = oldTab[j]) != null) {
//将老数组[j]位置置为null, help GC
oldTab[j] = null;
//如果e.next == 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);
//到了这个位置就只有一种可能,是链表
//这个时候需要注意hash & table.length-1的好处了,在上面hash函数说到,
//原先table[i]位置上的节点再扩容之后只会有两个位置
//1. newTable[i]
//2. newTable[i+OldCap]
else { // preserve order
//与1.7不同的是,因为旧节点在新数组的下标只能有两个
//就用两个链表将newTable[i]和 newTable[i+OldCap]位置的数据链起来
//loHead指向将插入newTable[i]的节点
//hiHead指向将插入newTable[i+OldCap]的节点
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);
//如果链表不为空,则直接将链表头部放入新数组newTable[i]位置上
//链表为空则旧数组的节点没有要放入newTable[i]位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//同上操作,两者只能有一个为空(要么全部放入newTable[i]位置上,
// 要么全部放入newTable[i+OldCap]位置上)
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//返回新数组
return newTab;
}
树的数据转移过程:
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
//先遍历原先树,用两个链表将数据连接起来.
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
if (loHead != null) {
//1.loHead != null -> 节点长度是否小于等于6(因为树的节点个数小于6要将树转为链表).
if (lc <= UNTREEIFY_THRESHOLD)
//若小于等于6,则将loHead变为一个**Node**类型的链表(**loHead定义为TreeNode类型**).
tab[index] = loHead.untreeify(map);
else {
//将table[i] = loHead; 接着判断hiHead是否为空,若不为空则将loHead变为TreeNode类型的树.
tab[index] = loHead;
//(注意:hiHead如果为空,则loHead就相当于原先的那棵完整的树,不需要在转化为树.
//而hiHead不为null,则loHead为原树的一部分数据,而loHead本身为链表的头,所以要转为一棵树).
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
//同上
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
//将TreeNode类型转为Node类型
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
//返回 q的Node节点类型
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
扩容过程:
1.获取原先table数组长度oldCap,扩容阈值oldTr
2.二倍扩容newCap = oldCap << 1,newTr = oldTr << 1
3.创建一个新数组newTab = new Node[newCap],将原先数组指向新数组
4.进行数据转移 (三种情况)
- 4.1.数组table[i]位置只有一个元素:直接将数据转移到新数组的位置(newIndex = e.hash & (newCap-1))
- 4.2.table[i]存放的是一棵树:同链表转移一样,上面说过,树其实也是一个双向链表,因为旧数组table[i]位置上的元素转移到新数组时存放的位置只有两种:一种是newTab[i],另一种是newTab[ i + oliCap].所以会发现定义了四个TreeNode节点:loHead,loTail和hiHead,hiTail,分别代表即将存在新数组两个位置的节点头和尾.
- 4.2.1 :然后遍历链表,计算节点在新数组的下标值(e.hash & oldCap),根据根据下标值确定节点添加在loHead还是 hiHead后面.对loHead和hiHead的节点个数进行记录.最后loHead链表连接的节点是要插入newTab[i]位置的,而hiHead链表连接的节点是要插入newTab[i+oldCap]位置的.
- 4.2.2 :这个时候链表loHead和hiHead可能会有一个为空,因为有可能节点在新数组的位置经过计算后都在一个位置,所以要进行判断:1. loHead != null -> 节点长度是否小于等于6(因为树的节点个数小于6要将树转为链表).若小于等于6,则将loHead变为一个Node类型的链表(loHead定义为TreeNode类型).将table[i] = loHead; 接着判断hiHead是否为空,若不为空则将loHead变为TreeNode类型的树.(注意:hiHead如果为空,则loHead就相当于原先的那棵完整的树,不需要在转化为树.而hiHead不为null,则loHead为原树的一部分数据,而loHead本身为链表的头,所以要转为一棵树). 2. hiHead != null 同上.
- 4.3.table[i]存放的是一个链表,其实和树转移操作类似,定义四个Node类型的节点:lohead,loTail,hiHead,hiTail,计算e.hash & OldCap,若等于0,则e将会存放在新数组的newTab[i]位置,用loHead将节点连接起来.等于1,则e将会存放在新数组 newTab[i+oldCap]位置,用hiHead将节点连接起来.最后将loHead,和hiHead放入新数组对应的位置即可.
删除方法
remove()方法
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
//判读数组是否为空,和table[i]位置是否为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//node指向待删除节点
Node<K,V> node = null, e; K k; V v;
//判断table[i]位置的节点是否是要删除的
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//如果是,将临时节点node指向 table[i]位置节点 p
node = p;
//不止一个元素
else if ((e = p.next) != null) {
//该位置存放的是一棵树
if (p instanceof TreeNode)
//从树里面查找
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
//该位置存放的是一个链表
do {
//遍历链表
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//判断是否找到了待删除节点
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//如果待删除节点在树里
if (node instanceof TreeNode)
//从树里面移除该节点
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
//如果待删除节点是第一个元素,将p.next放入table[i]
tab[index] = node.next;
else
//存在链表里面,将待删除节点node前驱的next->node.next
p.next = node.next;
//版本号加1(修改次数)
++modCount;
//数据个数-1
--size;
afterNodeRemoval(node);
//返回删除的节点
return node;
}
}
//没有找到或者,找到了但不进行任何操作,返回null,和putAbsent一个道理
return null;
}