一、HashMap的结构:数组+链表
1、那么数组在哪里?有多大?
我们来到HashMap的源码,可以发现它里面有个数组 transient Node<K,V>[] table;
数组的初始大小为16,static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
数组最大为2^30,static final int MAXIMUM_CAPACITY = 1 << 30;
2、数组已经明确了,那么链表呢?它的每个节点应该是怎么样的?
我们先自己想一想,应该有个key,有个value,有个next。再来看看源码中的实现,通过上面的数组,我们可以知道数组里面的每一个元素都是Node<K,V>,我们点进去,看看它的实现。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
//...
}
key,value,next都有了,很符合我们的预期。那么这个hash是用来干嘛的?是用来定位的,我要插入一个Node的话,它应该在数组的哪一个位置。
二、HashMap的插入
我们先想一想,把一个节点插入HashMap中,需要考虑些什么呢?
1)既然是数组+链表的结构,那么我插入的时候,数组有没有初始化呢?
2)定位。我这个节点应该放在数组的哪个位置上?如果这个位置上已经有元素了,那么跟在后面形成链表?
3)链表太长了,插入和查找的效率都很低,怎么办?
1、我们找到它的插入方法,put
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
我们先忽略hash(key)方法,先看putVal()方法
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);
...
}
我们看到n = (tab = resize()).length这一行,继续探索resize()方法。此方法中有很多if-else,此次我们只看数组初始化时,即table == null时。因此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;
newCap = DEFAULT_INITIAL_CAPACITY; //初始容量
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //数组使用了多少,才开始扩容
threshold = newThr;//threshold是实例变量,用来记录扩容阈值
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //初始化数组
table = newTab;
return newTab;
}
可以看到,正是在resize()方法里面,初始化了数组。解决了我们第一个问题,数组什么时候初始化。
2、resize()看了,我们再回到put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
再来看看hash(key)这个方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
我们来解释一下 (h = key.hashCode()) ^ (h >>> 16) 这句话
key.hashCode() 调用的是Object的hash算法,返回一个32位的数字。此时h有32位
h >>> 16,h向右移动16位
(h = key.hashCode()) ^ (h >>> 16) 将它们进行异或操作。作用是充分让h的每一位都参与进来,让Node节点尽可能地定位在数组的不同位置上
这句话的作用还是不理解?没关系,我们下面讲如何定位,还会提到(h = key.hashCode()) ^ (h >>> 16)的作用
3、我们回到putVal()方法的定位操作上
如果让我们自己想的话,我们可以会用 hash % (n-1) 来定位,而 %操作,并没有&操作来得高效。那让我们来解释一下,这个&操作吧
此时hash值,是我们上面看到的(h = key.hashCode()) ^ (h >>> 16),是个32位的数
n - 1 是15 (由于我们是第一次初始化,所以取的是初始默认容量,n=16)
可以看到hash的值只有最后的几位参与了运算,那多个不同的hash,只要最后几位相同,他们的位置不就重复了吗?数组的空间就不能充分利用了。
这也就是,为什么我们之前进行(h = key.hashCode()) ^ (h >>> 16)操作的原因了,让它的每一位都参与进来,让他们尽可能地定位在数组的不同位置上。
为了维持n-1的值,是1111, 11111, 111111这种形式,HashMap的容量规定是2的倍数
4、 如何定位我们已经知晓了,那这个位置上已经存在元素的话,hashMap将会做什么操作呢?
当数组的这个位置上已经存在元素的时候,它会在后面形成链表。当链表太长的时候,它会转化成红黑树。
static final int TREEIFY_THRESHOLD = 8; 这就是转化成红黑树的阈值,当链表的节点数量达到8的时候,进行转化
static final int UNTREEIFY_THRESHOLD = 6; 当红黑树的节点数量达到6的时候,又转回链表
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
Node<K,V> e; K k;
//1、数组上,如果hash值相等,key也相等,把这个位置的旧元素记下来,方便下面的新值取代旧值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
// 2、如果是红黑树结构,那么作为树的节点,进行插入
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 3、如果是链表结构,那么跟在链表的末尾
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1)
// 如果超过树化阈值,那么将链表转成树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//链表上,如果hash值相等,key也相等,把这个位置的旧元素记下来,方便下面的新值取代旧值
break;
p = e;
}
}
if (e != null) { // 是否hash相等,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;
}
5、我们已经知晓,当hash碰撞的时候,hashMap会形成链表或红黑树。那我们再来看看resize()方法,这次我们关注它的扩容操作,具体操作如下:
0)容量变为原来的两倍
1)新数组的创建
2)遍历原来的数组,将存在的元素,移到新的数组
2.1)如果是单个元素的话,那么hash值 & 新容量-1,定位到新数组的位置上
2.2)如果是红黑树的话,那么打散节点
2.3)如果是链表的话,根据hash值的不同,把链表拆成两个链表
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
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//1、新容量数组,创建
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//2、遍历原来的数组,将存在的元素移位到新数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//如果数组的这个位置上有元素,那么进行移位操作
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
//3、如果只有单个元素,不是链表和红黑树,那么定位(hash值 & 新容量-1)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//4、如果是红黑树的话,那么打散节点
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { //5、是链表的话,根据hash值的不同,把链表拆成两个链表
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;
}
6、待续
1)红黑树的结构
2)链表如何转红黑树
3)红黑树如何转链表
4)扩容的时候,红黑树如何打散