这是一篇很久以前写的文章了,当时是发布在了别的平台,今天搬来掘金。
构造
首先来看下hashMap的构造方法
HashMap hashMap = new HashMap();
复制代码
hashMap的无参构造方法非常简单,内部只是默认值的初始化,加载因子 0.75f
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
复制代码
直接来看下hashMap的两个参数构造方法,在默认值初始化的时候,tableSizeFor控制着hashMap size的大小,具体的实现下看:
public HashMap(int initialCapacity, float loadFactor) {
... // 一些校验判断
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
复制代码
可读性比较差,我们通过枚举几个cap值,来看下这个方法的作用:
static final int tableSizeFor(int cap) {
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;
}
复制代码
通过图示枚举的cap值,能够发现tableSizeFor 的作用,是为了保证hashMap size 是2的幂,例如我们new HashMap(7),底层实际分配的size 不是 7,而是2^3^ = 8。为何会如此,看下文。
put
再来看下hashMap的put方法,当然key和value可Object类型的
hashMap.put("key", "value");
复制代码
首先底层会通过key 来获取一个hash 值,这里的hash可不仅仅是key.hashCode(),还需要右移16位并做亦或运算。之所以要进行右移,是为了在获取下标 tab[i = (n - 1) & hash] 时,让hashCode的高位也参与到运算中,防止结果频繁发生碰撞。而 (n - 1) & hash 为何能当下标呢,它的取值范围是(0, n -1)吗?不一定,这就需要对n进行限制,就是上面说的必须是2的幂数。原理也不难,拿32位原码试下就行了,2^n^ -1 在 n (不包含)分割的低位上都是1,高位都是0,再进行&运算时,低位1才有用,而低位的组合结果范围就是(0, n - 1)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
复制代码
接下来底层开始调用putVal方法,hashMap的扩容逻辑都在这里。当首次oldTable 数组 null 或 容量 0 时,size会被初始化DEFAULT_INITIAL_CAPACITY = 16;当 oldTable 不为null,size 会被直接扩容 oldTable.length * 2 ,为何size 容量要扩容2 倍呢?看下文
final Node<K,V>[] resize() {
...
// table != null,更新 size 和 threshold
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // threshold * 2
...
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
...
// 更新threshold = 16 * 0.75f
threshold = newThr;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
}
复制代码
在oldTable不为null的情况:
当oldTab数组index上不存在碰撞,即没有链表,我们直接将oldTab[j] 随机放在newTab内,其下标位置取值范围e.hash & (newCap - 1),即(0, newCap - 1)
当有链表时,当随机的下标在index == 0碰撞,则将碰撞的元素插入到新链表尾部,当e.next null 时,将新链表移动到newTab[ j ] 位置上(注意这里的 j 其实就在原始的位置 j 上);当随机的下标未碰撞时,同样将链表内元素插入到一个新链表尾部,直到e.next null 时,将新链表移动到 newTab[j + oldCap] 位置上。而 j + oldCap 的取值范围(0, 2 * oldCap)。
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;
}
}
复制代码
上面的代码需要注意下TreeNode,这也是跟JDK7不一样的地方,在JDK8内引入了红黑树,在这里会判断当前的oldTab[j] 是否已经从链表转换成了红黑树了,如果是,则进行split操作。split的代码就不贴了,在内部有个方法需要注意下,当红黑树元素个数 < UNTREEIFY_THRESHOLD 时,即 lc < 6 时,又会将红黑树转换成链表。毕竟链表在插入操作上非常有优势。
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
复制代码
再得到当前的table后,接下来会随机获取存储的index: (n - 1) & hash ,范围(0,n -1),如果当前index无元素存储,则直接创建一个节点Node对象,放在该index下。所以在底层,虽然我们put进的是key与value,但实际保存的是Node对象。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
复制代码
如果 (n - 1) & hash 获取的index发生了碰撞。首先会判断:当前传入的key,是否已经存在,如果存在,之后e.value = value引用地址替换,则将oldValue返给用户。注意,通过(n - 1) & hash 来获取下标,如果hash值相同,那么就一定会发生碰撞,但是hash值相同,不一定value值相同,可能在当前数组index下的是一个链表,具有多个元素。
下看判断,当前如果是红黑数了,即 p instanceof TreeNod ,则直接会putTreeVal操作。
再下看判断:循环当前table[index]下的链表,如果节点e是链表尾部了,那么直接将当前put的value所构造的Node插到链表尾部,而在遍历过程中如果发现有相同的key存在了,break循环,引用地址替换,将oldValue返给用户。
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;
}
复制代码
在上代码内,需要注意下treeifyBin 方法,即当前链表的元素数量大于8时,需要做链表转换成红黑树的判断与操作。往下看
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
复制代码
在这里能看到,转换操作也是有条件的,当我们tab.length >= 64 时,我们才会进行数的转换,否则我们会进行扩容处理。因为扩容后,能增加整个tab的散列性,也能有效避免链表过长的问题,而当大于64时,出于内存的考量,转换成红黑数,能增加查询效率。
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) {
... // 转换
}
}
复制代码
get
hashMap的get操作,相比来说就简单多了。首先我们会拿到key的hash。
如果tab length > 0 并 当前下标有值存在,当tab[(n - 1) & hash] 下标值即key值时,直接返回该value,否则判断当前结构是否为红黑数,如果是则执行getTreeNode 获取对于value,如果当前结构为链表,则遍历查找相同key ,返回对应value
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
复制代码
好了,本文结束。