1.写在前面
前面的博客的我们简单的介绍完了红黑树,主要就是为了今天看1.8的HashMap的代码准备的,因为1.8的HashMap的源码有一个树化的过程,所以我们先简单的谈了下红黑树。
2.从put方法开始谈起
废话不多说,直接上代码,具体的put
方法源码如下:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
这儿我们调用的putVal(hash(key), key, value, false, true);
方法,这儿我们需要注意的是第四个和第五个参数。
- 第四个参数
onlyIfAbsent
:如果为真,则不要更改现有值,就是如果为真的时候,插入相同的键的时候,如果值不一样的话,则不会修改原来的键的值。很明显我们的HashMap是改变原来的值,所以这儿的值是false - 第五个参数
evict
:这个参数在HashMap
中没有使用,我们这儿可以不用纠结。这个参数主要是在LinkedHashMap
中使用。与今天的博客没有太大的关系,所以我们这儿直接跳过。
这儿我们还需要简单的了解下hash(key)
方法的源码,具体的如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
如果键为null
,直接返回0,所以HashMap
可以插入键为null
的值。
如果键不为null
,这个时候值等于先将这个键的hashcode
计算出来,然后将这个值右移16位,然后在异或。这儿我们也不用深究其中的含义。我们只知道是这样计算出来的即可。
上面的方法大概讲完了,这个时候我们继续看源码,这个时候我们需要查看putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
方法,具体的代码如下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
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) {
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;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
上面的方法比较长,可能比较难搞,这个时候我们只需要分成几种情况,这样就比较好理解,这儿我们已经将对应的分支分开了,只要讲几种情况就行了。
2.1创建HashMap
这种的情况,主要是HashMap没有创建,刚刚开始创建,于是会进入第一个分支,具体的代码的如下:
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
这儿我们看的resize
方法,具体的代码如下:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
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;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = 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);
else { // preserve order
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);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
一看方法这么长,是不是想死的心都有,不过不用担心,因为这个方法有两个职责,所以这个方法比较长,这个方法主要用于创建HashMap
同时还有一种职责扩容,我们由创建的HashMap
进入这个方法的,所以很多的代码我们暂时不用看的,只需要看我们创建HashMap
的代码就行,于是代码精简成如下的内容:
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
/*if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;*/
else { // zero initial threshold signifies using defaults
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;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
return newTab;
为了提高代码的可读性,所以我们这儿将不调用的代码注释掉了。
这儿主要四个比较重要的变量,分别是oldCap
扩容前HashMap的容量,oldThr
扩容前HashMap
的负载因子乘以容量,newCap
扩容后HashMap
的容量,newThr
扩容后的HashMap
的负载因子乘以容量。这儿这4个值都是为0,因为这儿的HashMap还没有创建。那么这个时候,我们就需要创建这个HashMap,这个时候我们需要知道几个常量,具体的如下:
- DEFAULT_INITIAL_CAPACITY的值为16
- DEFAULT_LOAD_FACTOR的值为0.75
于是将DEFAULT_INITIAL_CAPACITY
的值赋值给newCap
,然后将DEFAULT_INITIAL_CAPACITY
与DEFAULT_LOAD_FACTOR
的乘积赋值给newThr
然后创建对应的长度的Node
的数组返回,同时将这个创建好的值赋值给全局的变量table
。同时将newThr
这个变量赋值给全局的变量threshold
。至此整个创建就完成了。
2.2插入(没有hash冲突的情况)
回到我们的原来的方法,我们继续看下一个分支,具体的代码如下:
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
这儿我们需要的知道的这个数组的下标我们是怎么知道的,主要通过下面的式子计算出来的,具体的如下:
i = (n - 1) & hash
这儿的n是16然后减去1就是15然后与上hash的值,这个时候我们会得到一个下标,然后获取这个数组对应的下标的这个值,如果为空,就直接创建这个Node节点。这个时候有一个疑问,**我们都知道前面的方法创建的这个HashMap的Node的数组的长度就是16,那么我们怎么一定肯定计算出来的这个下标就一定是0到15之间的值呢?**这个问题稍后再回答。我们先看看 newNode(hash, key, value, null);
方法,具体的代码如下:
tab[i] = newNode(hash, key, value, null);
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
需要注意的是这儿这个Node的这个对象的下一个节点为null,也是没有问题的,因为这儿就是为null,这个下标首次插入对应的值,它的下一个节点就是为空。
回到刚才的问题,我们都知道前面的方法创建的这个HashMap的Node的数组的长度就是16,那么我们怎么一定肯定计算出来的这个下标就一定是0到15之间的值呢?
我们需要再次看一下计算hash
的方法,具体的代码如下:
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
h >>> 16
h 0000 0000 0000 0001 0000 0000 0000 0000
h >>>16 0000 0000 0000 0000 0000 0000 0000 0001
这儿右移16位的话,就是int的值的高16位移动低16位,我们再来看下异或
(h = key.hashCode()) ^ (h >>> 16)
h 0000 0000 0000 0001 1000 0000 0000 0000
h >>>16 0000 0000 0000 0000 0000 0000 0000 0001
^ 0000 0000 0000 0001 1000 0000 0000 0001
也就是hashcode
的值的高16位于16位异或这个值作为结果值的低16位,然后原来的hashcode
的值的高16位不变作为结果值的高16位
这个时候我们再来看下标的计算方法
i = (n - 1) & hash
15 0000 0000 0000 0000 0000 0000 0000 1111
hash 0000 0000 0000 0001 1000 0000 0000 0001
& 0000 0000 0000 0000 0000 0000 0000 0001
由于是与运算,所以这儿不管是什么值,与上15一定是0到15之间的值。
2.3插入(有hash冲突的情况)
有hash冲突的情况下,又有两种情况,一种是链表的插入,一种是红黑树的插入。我们先看链表的插入
2.3.1链表的插入
Node<K,V> e; K k;
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) {
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;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
为了代码的可读性,我又注释了不用的代码,然后我们看对应代码。
这儿也是两种情况,我们先看插入的key
是一样的
- 这儿的p是我们查找出来的
Node
,如果这两个Node
的hash
的值是一样的,同时这两个Node
的值key
是一样的,那么我就用e接受这个查找出来的Node
。 - 然后根据
onlyIfAbsent
的值,如果这个值为false
,我们就修改相同key
的值,同时将这个key
的原来的value
的值返回出去。
第二种情况,就是插入key的是不一样的
-
这个时候我们直接进入else的的分支中去。先遍历这个找到的节点对应链表的尾部,然后直接插入进去。这个时候我们需要知道常量
TREEIFY_THRESHOLD的值为8,也是说当链表的长度大于8的时候,我们需要转成红黑树。
-
这个时候在遍历的时候可能找到对应的key是一样的,这个时候直接结束循环,然后走剩下的流程,就是和第一种的情况差不多。
这个时候我们需要看看这儿怎么将链表转换成红黑树的方法treeifyBin(tab, hash);
,具体的代码如下:
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
这个时候我们进来第一个if的分支,然后我们发现数组的长度是小于**MIN_TREEIFY_CAPACITY(64)**这个时候我们继续看resize()
方法。具体的代码如下:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
/*if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}*/
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
/*else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
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;
return newTab;
}
扩容的代码我已经将这次流程不走的代码注释掉了,只看主要的流程。首先还是这四个变量。不懂的可以看看前面的介绍这4个变量,这儿不做过多的赘述。首先进入的是第一个if的判断,newCap
变成原来的oldCap
的两倍,前提有一个条件就是newCap
小于MAXIMUM_CAPACITY(int的最大的值)
同时oldCap
大于DEFAULT_INITIAL_CAPACITY(16)
这个时候newThr
和newCap
变成了原来的两倍了。
然后就是一些赋值的操作,这个时候我们需要继续看剩下替换的功能。具体的代码如下:
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = 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);
else { // preserve order
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);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
上面的代码主要是重新分配下HashMap
的值。主要分成三种情况
- 第一种情况:就是这个数组的下标的值就只有一个值,这个时候只需要重新计算一下下标就可以了。然后放到新的数组中去。
- 第二种情况:就是这个遍历出来的元素,是一个树节点,就直接调用
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
方法。 - 第三种情况:就是这个遍历的出来的原始,是一个链表,遍历这个链表,将这个链表分成两个链表,主要是这个键的hash的值与上这个原来的数组的长度,如果为0的话,就放到
lo
的链表中,如果不为0的话,就放到hi
的链表中去。计算完了,将lo
的链表挂到新的HashMap
中去,下标是当前遍历的下标中去。将hi
的链表挂到新的HashMap
中去,下标是当前遍历的下标加上原来HashMap
长度的下标中去。
最后再回到原来的树化的流程,具体的代码如下:
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
/*if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();*/
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
先将这个节点转换成TreeNode节点,然后遍历这个链表,转换成双像链表,同时将这个链表转换成功红黑树,主要调用的方法是hd.treeify(tab);
2.32红黑树的插入
Node<K,V> e; K k;
/*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) {
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;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}*/
主要调用的是 ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
方法,由于篇幅的问题,有关红黑树的操作,我们下篇博客我们继续讲。
3.总结
具体的流程图如下:
4.写在最后
这篇博客主要介绍了1.8JDK的HashMap
的方法,大概的介绍了流程,有关红黑树的操作,下篇博客我们继续介绍。