1.8之前的HashMap存储数据使用了数组+链表方式
1.8之后的HashMap存储数据使用了数组+链表+红黑树的方式
声明:本文基于jdk1.8分析
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
构造了初始容量16,加载因子0.75的一个空的HashMap。
接着我们调用put方法第一次往HashMap中存放数据
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
这里先调用了hash方法计算hash值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这是一个扰动函数,将key的hashCode和该hashCode的高16为进行异或计算出hash。后面会通过hash和数组长度-1进行&运算,来计算出当前要put的数据放入数组哪个桶中。比如当前我们初始容量16,也就是2的4次方。(16-1)的二进制刚好低4位都是1( 0000 0000 0000 1111),所以hash&(16-1)刚好就是取低4位。容量为32也就是2的5次方,低5位都是1。前面进行扰动函数就是为了让计算后的hash具有hashCode高位和低位的特征,降低hash碰撞的概率。
知道了hash方法的计算原理后,继续看putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//这些局部变量会在下面的if语句中赋值
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//注释1:如果table是空的,就初始化数组
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
//注释2:要放在数组中哪个位置,且该位置当前是空的
tab[i] = newNode(hash, key, value, null);
else {
//注释3
//...这个逻辑先不看
}
++modCount;
//如果hashmap中的节点大于threshold就扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
先看注释1代码:因为我们是第一次put,所以tab是空的,此时需要先初始化数组,也就是调用resize()方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//初始化时候oldCap=null,oldThr=0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//..先不看
}
else if (oldThr > 0) // initial capacity was placed in threshold
//..先不看
else { // zero initial threshold signifies using defaults
//初始化newCap=16 newThr = 12(16*0.75)
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//创建node数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//将node数组赋值给成员变量table
table = newTab;
if (oldTab != null) {
//...先不看
//初始化不会走这个逻辑,该逻辑用来处理扩容后,将旧数组中的所有数据迁移到新数组。
}
return newTab;
}
在看注释2代码:首先通过(n - 1) & hash计算出要放在数组中哪个位置,如果该位置当前是空的,那么就创建一个新的Node,,并放入数组对应桶中。
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
这样我们第一个数据就被插入HashMap中了。接着当我们继续插入数据。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//这些局部变量会在下面的if语句中赋值
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)
//如果数组中位置i对应的桶没有数据,就把要插入的数据放入该桶中
tab[i] = newNode(hash, key, value, null);
else {
//如果数组位置i的桶不是空的,也就是有node了,那我们新的数据就以节点的方式插入链表尾部或者红黑树中
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//如果要插入的数据的key和数组中元素的key相同,那就把它赋值给变量e
e = p;
else if (p instanceof TreeNode)
//如果数组中的节点是TreeNode类型的,说明数据结构已经转为红黑树了,就调用TreeNode的putTreeVal方法插入
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果是链表形式,就从链表头部开始,也就是数组中的那个,不停往后遍历
for (int binCount = 0; ; ++binCount) {
//如果找到链表最后一个节点,并且没有找到和要插入数据key相同的,就创建一个新节点,插入链表尾部(尾插)
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果链表中节点树大于等于TREEIFY_THRESHOLD,链表会进行树化,链表-->红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果在遍历过程中找到了某个节点和要插入数据key相同,就跳出循环 次数变量e就是该节点。
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果e不为空,就说明新数据的key在hashmap中存在相同的,此时直接用新value替换旧的value就好了。
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
//返回旧value
return oldValue;
}
}
++modCount;
//如果大于阈值,就扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
在我们往HashMap插入数据时,首先会通过(n - 1) & hash计算出要插入在数组中的位置,如果该位置还没数据,就直接放入。如果已经有了,那就遍历数组该桶中所有的节点,它们以链表或者红黑树的方式存储。如果在链表中没有找到就创建新节点插入链表尾部,如果找到有相同key的节点,那就替换掉value。插入时如果链表长度大于TREEIFY_THRESHOLD此时链表会转为红黑树。
关于红黑树处理会涉及它的自平衡,变色和旋转,本篇不会做讲解,感兴趣的可以自己去了解红黑树相关知识。
每次插入数据后都会判断HashMap的size是否大于阈值,如果大于就会调用resize()方法进行扩容处理,注意size是HashMap中总节点数量,扩容是扩容我们数组的长度。
之前我们了解了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;
//如果旧的数组长度大于0,此时我们进行扩容
if (oldCap > 0) {
//如果容量已经达到最大值了,不能进行扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//在旧数组容量基础上扩容两倍,比如16-->32
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//新的阈值时原来的两倍,比如12-->24
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;
//下面代码是将旧数组中的所有节点迁移到新数组中,需要重新根据hash&(newCap - 1)计算放入的数组下标
if (oldTab != null) {
//遍历旧数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//如果数组中的元素不为空,就赋值给e
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//数组中该位置只有一个节点,直接根据hash和新数组长度计算到要放入的新数组中的下标,直接放入即可
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
//处理节点是链表的情况
//loHead低位头节点,loTail低位尾节点
Node<K,V> loHead = null, loTail = null;
//hiHead高位头节点,hiTail高位尾节点
//低位代表新数据下标为i的位置,高位代表新数组下标为j + oldCap的位置
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//不停的遍历链表中的节点
//对于节点hash&oldCap == 0的情况,组织成链表,头节点是loHead
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//对于节点hash&oldCap != 0的情况,组织成链表,头节点是hiHead
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//把loHead放入新数组j位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//把hiHead放入新数组j + oldCap位置
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
上面的代码中e.hash & oldCap 是否为 0来判断该节点是放在新数组的低位和高位逻辑看起来挺唬人的,其实就是对e.hash & (newCap - 1)的优化,效果是一样的。我们可以做个测试:
//测试代码
public static void main(String[] args) {
int hash = 1111;
int oldLength = 32;
int oldIndex= hash & (oldLength - 1);
System.out.println("oldIndex = "+oldIndex);
System.out.println("通过hash & oldLength来计算在新数组中的位置");
int flag = hash & oldLength;
if (flag == 0){
//低位
System.out.println("应该放在新数组中的位置(低位):"+oldIndex);
}else {
//高位
System.out.println("应该放在新数组中的位置(高位):"+(oldIndex+oldLength));
}
System.out.println("\n");
System.out.println("通过hash & (newLength - 1)来计算在新数组中的位置");
int newLength = 64;
int index2= hash & (newLength - 1);
System.out.println("应该放在新数组中的位置:"+index2);
}
打印结果:
oldIndex = 23
通过hash & oldLength来计算在新数组中的位置
应该放在新数组中的位置(低位):23
通过hash & (newLength - 1)来计算在新数组中的位置
应该放在新数组中的位置:23
修改hash 为 2222,打印结果如下:
oldIndex = 14
通过hash & oldLength来计算在新数组中的位置
应该放在新数组中的位置(高位):46
通过hash & (newLength - 1)来计算在新数组中的位置
应该放在新数组中的位置:46
多次测试可以发现hash & (newLength - 1)的结果总是和oldIndex或者(oldIndex+oldLength)相等。
到这里关于HashMap主要的流程就分析完成了,这里做个总结: