发现HashMap的源码和自己原本看到的文档不同,所以决定看看到底Java是如何实现HashMap的。
本文所使用的环境为
利用Idea提供的功能直接跳转到HashMap.put();的源码页面
HashMap map = new HashMap<>();
map.put("1", "1");
接着往下看,如果说这个HashMap第一次调用了put函数,会有一个resize的操作来初始化实际上存储对象的Node数组。
按照代码给的注释来说。这个resize函数,会在初始化的时候新建一个存储对象的数组(16的容量),也用在HashMap扩容的时候,会以原容量两倍的规模进行扩容,原来的Key要么保持原位置不变,要么在扩容后的数组里移动2的幂次的位移量(这一点说法有些迷惑)。
Node[] 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[] newTab = (Node[])new Node[newCap];
table = newTab;
```
在函数的一开头,会对老的存储结构做一个检查,看看到底是要初始化一个新的存储结构,还是对老的存储结构容量达到了预定值而需要扩容。放弃边界条件,粗暴一些理解的话,就是全新的HashMap会按照16的规模进行数组的新建,达到预定值 threshold时, 会按照老容量 x2 进行扩容。
在扩容之后,老数据自然不能不要,所以需要把老数据迁移到新的扩容过的数组中,源码中的做法就是走一个For循环,一个一个搬运。
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node 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)e).split(this, newTab, j, oldCap);
else { // preserve order
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node 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;
```
可以看到在搬迁单个bucket,或者说数组中的一项的时候,做一个 e.next == null 的检查,这个就是我们常说的HashMap中的碰撞策略是用链表来实现的。当然如果经常发生碰撞的话,我们也知道HashMap的复杂度将退化为O(n),所以Java8中会对同一个bucket中,如果链表长度大于等于7的时候,会将其转化为树结构,进行优化,这一点会体现在新的键值对加入到HashMap时做检查。
在搬迁这样的链表时,HashMap会检查这个键值对Key的hash值(非hashCode,而是HashMap专用的一套Hash算法),如果说他和老容量与计算不为0的话,就会移动老容量的距离。
老数据容量为16,oldCap = 16,当 j = 1 时,遇上了链表,此时会逐一遍历链表。当链表节点的hash值 和 oldCap(16)与计算等于0的时候,比如说 例子中的第一个节点 hash值为1,这个节点的hash并不代表他在数组中的位置,数组中的位置是用该hash值和 oldCap-1 与计算算出来的,所以1 & 16 = 0 ,他的位置没有改变,而17&16得出的结果为16,这个节点就被转移到了 1 + 16 = 17 的数组位置上去了。
这样的目的很容易看出来是为了优化链表结构,防止其推到O(n)的复杂度。
值得注意的时候,这个结果如果已经被转化为了树结构的话,则是利用树节点的插入方法。
HashMap的resize()方法就这么多。话题转回putVal方法。
Node[] tab; Node p; int n, i;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node 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)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;
}
}
对于Hashmap的put()方法来说,会先对Key做一个哈希化,然后再与当前存储容量-1做一个与运算,最后得出的结果才是这个键值对的存储位置。如果说这个位置上还没有值,那么直接将他放在这个位置。如果有了值,那么就是发生了hash碰撞。在常规的Hash方法中会再做一个hash直到没有碰撞位置。但是HashMap则是使用了包容的态度,将这些键值对都放在一个位置上。当然,如果两对键值对,Key的Hash值一样,内存位置一样,或者内存存储值一样,那么我们可以认为他们就是一样的,这个时候会直接替换,这么一想挺可怕的,如果这个位置上是一个链表的话,整个链表就没有了。(就像一样食物看起来像汉堡,尝起来像汉堡,闻起来像汉堡,那他一定是老八蜜汁小汉堡)
//(另注:可能这里就会有多线程错误,如果一个键值对在链表里插入,而另一个线程发生这样的替换)(待验证)
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
代码中这一样,就是在做链表到树结构的转换。
在一个我自己觉得有意思的就是remove操作,或者说如果你在别的地方另外建了一个Key,他怎么定位到之前你put到HashMap里的。我们都知道如果你要将一个自建类作为HashMap的话,你一定要新写一个hashCode()的方法,原因就是找到具体位置是和类的hashCode()方法有关系的。
/**
* Removes the mapping for the specified key from this map if present.
*
* @param key key whose mapping is to be removed from the map
* @return the previous value associated with key, or
* null if there was no mapping for key.
* (A null return can also indicate that the map
* previously associated null with key.)
*/
public V remove(Object key) {
Node e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
```
remove()方法实际判断位置和put()一样有两个,hash(key)和key。
final Node removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node[] tab; Node p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode)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)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
顺着代码读下来,基本逻辑和putVal一直,如果数组中某一个位置的起始节点就已经是目标键值对了,就直接让他指向第二个节点,即使他是null。如果不是的话,就会遍历查找,找到之后,再做remove去除操作。所以说,如果不重写hashCode方法的话,有两种可能,一种是把别的位置上的键值对去除了,另一种就是定位到了一个空的位置上,当然结果是一样的,你的remove失败了。
比较好奇的是,当数组元素减少,树结构居然没有变链表结构的代码。