hashMap中定义了一系列变量,我们先来解读一下hashMap中定义的几个变量的含义
// 这是hashMap的默认容量,必须为2的幂,至于为什么是2的幂,后面我们会提,默认是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 这是容量的最大值,容量不能超过这个值
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子,表示hash允许的饱和程度
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表树化阈值,我们都知道hashmap中链表过长会向红黑树转化,这个值表示当链表长度超过时就会向红黑树转化
static final int TREEIFY_THRESHOLD = 8;
// 链表过长会向红黑树转化,同理,树的长度过短也会向链表转化,这个值就表示当红黑树长度向链表转化的最小长度
static final int UNTREEIFY_THRESHOLD = 6;
// 最小树化数组长度,看过一部分源码的都会觉得链表长度大于8时就会向红黑树转化,实则不然,有一个前提条件,就是数组长度要大于一个阈值
// 这个值就表示链表向红黑树转化时数组的最小长度
static final int MIN_TREEIFY_CAPACITY = 64;
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
点击put方法进来,我们看到put方法内部调用了putVal方法,我们接下来分析putVal方法
首先计算hash,这个hash的计算很巧妙,从源码来看是将key的hashCode值与hashCode值无符号右移16位进行异或运算得到,至于为什么要这样计算hash值,目的是为了减少hash冲突,至于为什么能减少hash冲突,我们等会解释
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
我们知道1.8中hashMap是数组+链表+红黑树的结构,这里table变量便是这个数组,首先判断数组长度,如果数组为空或者长度为0,调用resize方法进行数组初始化,默认数组长度为16;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
如果数组初始化了,计算key的下标位置,这里通过上述得到的hash值与数组长度减1进行&运算来得到下标位置,为什么这样能够得到下标位置?还有就是为什么能保证计算出来的位置不会超出数组的长度?
请看,假设数组长度为16
hash: 0101 1010
n-1 : 0000 1111
i : 0000 1010
由于数组长度前面我们提到一定是2的幂,所以n-1的二进制表示一定是 0000 1…1的形式,由于n-1的前几位是0,又因为是&运算,所以最终运算结果,n-1为0的部分一定是0,不为0的部分则是n-1不为0的部分对应hash的部分。这个范围刚好是0~n-1,也就是长度为n的数组的索引范围,所以为什么这样计算得出的下标不会超出数组索引,如果数组长度不为2的幂这里得出的索引就没有这个规律,因而限定数组长度为2的幂;另外我们注意到最终结果的得出只与hash的后面部分有关,这样就会有一个问题,这样不会大大提高hash冲突的概率吗?这也是为什么hash值的得出要使用hashcode与hashcode右移16位运算来得出,hashcode是32位,右移16位刚好是hashcode的高位和低位都参与运算来得出的hash值,这样在后面计算索引位置时尽管仍然是hash的后面部分参与计算,但是hash是hashcode高位与低位运算所得出,因而能降低hash冲突的概率。
计算出元素的位置后,判断该位置是否存在元素,如果该位置没有元素,就直接插入元素
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
如果该位置有元素,比较原有的key和新插入的是否是同一个key
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
如果不是同一个key,判断当前元素是不是树节点
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;
}
}
遍历链表,如果链表中不存在要插入的key,则将元素插入到链表末尾(尾插法,jdk1.7是头插法),插入到链表后,会进行链表长度判断,如果链表长度大于树化阈值(TREEIFY_THRESHOLD),调用treeifyBin方法,treeifyBin方法内部会进行判断,只有数组长度超过最小树化数组长度(MIN_TREEIFY_CAPACITY)才会进行链表向红黑树的转化,所以并不是链表长度大于树化阈值(TREEIFY_THRESHOLD)链表就会转化为红黑树;而如果数组长度没有超过阈值只会进行hashMap的扩容。
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
这里在判断要插入的key是否已经存在,如果存在就覆盖原有value,并返回原有value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
如果要插入的key不存在,程序就会走到这里,这里已经插入了新的key,会判断是否需要扩容,判断当前元素是否超过扩容的阈值,扩容的阈值由数组容量乘以加载因子得出(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY),当元素个数超过这个值就会进行hash的扩容。
++modCount;
if (++size > threshold)
resize();
// 这个方法由HashMap子类LinkedHashMap实现,探讨hashMap不用关注
afterNodeInsertion(evict);
return null;
至此,关于hashMap的put过程依据源码已经分析完毕,如果理解错误,欢迎指正