JDK8源码解读:HashMap
Posted 开源java学习
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JDK8源码解读:HashMap相关的知识,希望对你有一定的参考价值。
在使用HashMap操作自如后,对:hashCode、hashMap结构:数组列表、散列冲突、装填因子有了一定的概念,通过分析HashMap源码中put()方法,深入理解HashMap的工作原理及应用范围。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
put方法会return一个putVal方法,方法包含5个参数,其中前三个:
hash(key):是根据传入的key键来计算出一个int型的数字
key:要放入hashMap中的【键】
value:和键相对应的【值】
h = key.hashCode()这里h是这个key通过hashCode()方法计算得到的hashCode值。得到h后:h ^ (h >>> 16) 即表示h会进一步和 (h >>> 16)进行异或运算并return最终的结果。这里细心留意下会发现:h是个32位的int型,h >>> 16表示:原先位于右半部分的低16位全被清空,原先位于左半部分的高16位移到了现在的右半部分,左边空位用0补齐。
然后得到的数再和h进行异或,那么最后的结果相当于保留了原先h的高16位部分,而低16位部分则相当于用原高16位和低16位异或。
hash(key)计算出键的hash值后,put方法return一个putVal()方法:
return putVal(hash(key), key, value, false, true);
putVal方法初始化了几个变量:
Node<K,V>[] tab; Node<K,V> p; int n, i;
看到Node,进去看一下是如何定义的:
static class Node<K,V>
implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value,
Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
可以看到,和LinkedList中的Node类似,HashMap中也定义了内部类:Node<K,V>,不过有点不同的是Node节点类有4个成员变量:
final int hash;
final K key;
V value;
Node<K,V> next;
每个Node节点都可以存一个int类型的hash值,key,value,和指向另一个Node节点的引用next,现在让我们回到putVal中看一下:
Node<K,V>[] tab; Node<K,V> p; int n, i;
这个tab是个Node节点数组,里面肯定有联系,带着疑问我们接着往下看:
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
越看越晕啊,table是什么?
transient Node<K,V>[] table;
这个table是个Node节点数组,HashMap的底层是链表数组,这个Node<K,V>[] table就是HashMap所谓的【链表数组】
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
将table赋给tab,若tab为空或tab.length == 0,则执行n = (tab = resize()).length;
如果初始的HashMap为空则tab == null则进入初始化过程,初始化主要是通过resize()执行。
resize()后的数组列表赋给tab完成初始化。resize方法,看名字就知道是用于扩容的方法,现在知道resize()方法是用来给HashMap扩容的。经过resize()方法初始化赋值后tab成为了一个长度为16,阈值为12的数组列表(初始容量默认为16,装填因子默认为0.75,阈值 = 容量 * 装填因子)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
如果tab[i]对象为空,则通过newNode()方法构造一个新节点,然后插入tab[i]中。注意,此处索引
i = (n - 1) & hash
是hashMap中的核心知识点。对于任意一个新的等待添加的元素,是如何计算其插入位置索引的呢?就是通过(n - 1) & hash.hash是之前通过hash(key)计算出来的int型的hash值,n为table的容量 = table.length。
结合上面int型的hash值的算法:
h = key.hashCode()) ^ (h >>> 16)
说明这里的hash值h是和hashCode相关的,但是不等价,且计算方式比较独特。这是因为在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。
这个方法计算非常巧妙,它通过
h & (table.length-1)
来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,因为&比%具有更高的效率!
HashMap中的哈希碰撞问题
首先来定义哈希碰撞问题:
table[i]处原先没有对象时可以通过
tab[i] = newNode(hash, key, value, null);
插入新对象。要是tab[i]处不为空该怎么办呢?
此处就是hash碰撞:即不同元素的key通过hash(key)散列出的索引i相等,导致这些元素都会插入至table[i],也就是产生了哈希碰撞。
//如果当前待插入对象的hash值和table[i]对象的hash值相同,且key值相同,则用新value覆盖掉原value
if (p.hash == hash &&((k = p.key) == key
|| (key != null && key.equals(k)))){
e = p;
}
//若key不相同,且table[i]为红黑树,则将节点放入树中
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//若key不相同,且table[i]为链表,则进入for循环
else {...}
1.若当前对象的hash值和tab[i]对象的hash值相同,且key键也相等,则直接用当前对象的新value值覆盖原value。
2.若hash值相同但key不同,则将当前对象插入到table[i]中的对象中去,此对象可能是链表也可能是红黑树。若table[i]对象为红黑树,则将当前对象插入树中。
3.若table[i]对象为链表,则插入之前需要依次遍历每个链表节点,寻找插入位置。
简单来说,如果table[i]为链表,那么会通过一个无限for循环遍历此链表来寻找插入位。正常情况下会循环到链表尾,停止循环,将待插入的key和value通过newNode()方法放入新构造的Node节点中,将此Node节点置于链表尾。或者在循环过程中发生key值冲突,则会提前break出for循环。这时,最后如果e != null,则表示发生了key值冲突,则会用新value值覆盖掉原节点的value值,并return出去。
点击关注,学习更多java源码知识!
以上是关于JDK8源码解读:HashMap的主要内容,如果未能解决你的问题,请参考以下文章