1,HashMap的概要
HashMap内部使用了哈希函数,是关联数组哈希表,是线程不安全的,它允许自己的key为null,也允许自己的value为空,遍历时无序.
其内部的哈希桶是数组,数组的话就会涉及到扩容操作,每个哈希桶都放的都是链表,链表的结点,就是hash表的元素.在JDK1.8中,当链表的结点个数达到8个时,就会将链表转化为红黑树,以提升它的查询和插入的效率
他实现的接口有Cloneable,Serializable, Map
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
然后我们来谈谈扩容的问题,对应扩容我们涉及到一个值Threshold(阈值),当容量超过Threshold的时候,就会进行扩容操作,扩容的前后都是2的次方.因为都是2的次方,所以我们在通过key寻找value时,我们就可以通过位运算来代替普通的取模运算.
对应key对应的hash值,不仅仅是key.getHashCode()这个方法,还要经过扰动函数,使得Hash更加的均衡.HashCode的范围是40多亿,而且是int类型,是很发生碰撞的.
但是我们要考虑到,hashMap的桶远远要比Hash的取值范围要小很多,所以我们会根据桶的长度进行取余,忽略高位,只是用hash的地位,这样的话,碰撞的几率就会大了很多.
此时我们就需要引入扰动函数,它综合了高位和低位的特征,并全部都放在了低位,这样相当于高低位都进行了运算,通过这个来减少hash碰撞的几率,扩容操作时,会new一个新的Node作为hash桶,这是我们将原来所有的值,全部put到new的node中,重新做了一个put操作,性能消耗非常大,所以当Hash的内容越大时,性能消耗就越明显.
当发生过hash碰撞,且节点数小于8个(即在一同链表中),这是我们将结点放入新的hash中,有可能保持不动(low位),也有可能原位置+原哈希桶的容量(high位)
在HashMap的源码中,有许多位运算代替常规运算的地方,以此来提升效率
Hash&(arr.length-1)代替了Hash%arr.length
通过if((e.hash&oldCap)==0)判断扩容之后e是在低区还是高区
了解完了HashMap的基础知识,我们开始来研究HashMap的源码吧
2,链表的结点类型
static class Node implements Map.Entry {
final int hash;//此结点的hash值
final K key;//此结点的key
V value;//对应的value
Node next;//因为是链表,所以要指向当前结点的下一个结点
Node(int hash, K key, V value, Node next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
//key的哈希值异或上value的哈希值
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
//设置结点的value
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry,?> e = (Map.Entry,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
了解了Node的属性和方法,十分的清晰明了,我们只要知道其中的属性即可,因为是链表的数据结构,所以我们使用next
3,HashMap的基本属性
//最大的容量是2^30方
static final int MAXIMUM_CAPACITY = 1 << 30;
//初始化容量为2^4方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//默认的加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//hash桶
transient Node[] table;
//加载因子,上面默认的加载因子是0.75f
final float loadFactor;
//域值=加载因子*当前哈希桶的大小
int threshold;
4,HashMap的构造函数
//无参构造函数,只是将构造因子设置成默认的构造因子
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//此构造函数,指定了HashMap的容量
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//指定初始容量和加载因子
public HashMap(int initialCapacity, float loadFactor) {
//不合法值的处理
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//如果超过了2^30次方,那就设置成最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//负载因子不能使负数
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
我们注意到最后调用了tableSizeFor这个方法,我们在进入到这个方法中
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;
//上面的五行,经过运算使得每一位都是1,这是在此基础上再加上1,就一定是2的次方
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
然后我们来看最后一个hashMap的构造函数
public HashMap(Map extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
第一句话设置了默认的构造因子,我们来详细的研究一下最后一行的方法,我们进入到其中
final void putMapEntries(Map extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
//由加载因子计算出当前的阈值
float ft = ((float)s / loadFactor) + 1.0F;
//处理边界条件
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//如果新的阈值大于原来的,那么返回一个满足2的次方的阈值
if (t > threshold)
threshold = tableSizeFor(t);
}
//如果传入的m的值大于当前的阈值,就会进行扩容操作
else if (s > threshold)
resize();
//处理完其他的工作,就是将m中的key value依次的移动向当前的table
for (Map.Entry extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
5,扩容操作
5.1 扩容操作的第一部分
final Node[] resize() {
Node[] oldTab = table;//获取到当前的hash桶
int oldCap = (oldTab == null) ? 0 : oldTab.length;//获取到当前的长度
int oldThr = threshold;//获取到当前的阈值
int newCap, newThr = 0;
//首先我们都newCap和newThr的进行分析
if (oldCap > 0) {//如果原hash桶的容量不为空
//边界分析,原容量已经超过了最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;//阈值变成了2^32 -1
return oldTab;//直接返回,不用在创建了
}//如果在最大值之内,那么新的容量就是旧的容量的2倍
//如果旧容量大于初始容量
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//那么阈值就变成原来的二倍
newThr = oldThr << 1; // double threshold
}//当当前表示空的时候,新的表的容量直接就等于了就得阈值
else if (oldThr > 0)
newCap = oldThr;
else { //如果旧的表容量为空,阈值也是空的,那么新表就相当于是初始化一张Map
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;
总结一下,对于扩容的newCap,和newThr的修改如下:
(1),如果原表不是空的
(1),容量已经到了最大值,直接返回
(2),如果没有到达最大值,新容量为原容量的二倍,新阈值也是原阈值的二倍
(2),如果表是空的,但是有阈值
新的容量就是旧的阈值
(3),如果表示空的,也没有阈值
新的容量就是默认16,新的阈值就是默认12
(4),如果新的阈值是空的,那就根据新的容量计算出阈值出来
5.2 扩容的第二部分
我们这个时候已经获得了newCap和newThr的值,我们开始移动元素
Node[] newTab = (Node[])new Node[newCap];//首先初始化新的Table
table = newTab;//将当前类的对象的属性指向这个newTab,因为这是我们将来的HashMap中的桶
if (oldTab != null) {//如果旧的桶不是空的
for (int j = 0; j < oldCap; ++j) {//我们就开始遍历
Node e;
if ((e = oldTab[j]) != null) {//这时候e就是链表的头结点
oldTab[j] = null;//将原来的设置为空,这样jvm就会根据GC将其回收
if (e.next == null)//如果只有一个结点,此时不会发生哈希碰撞
//当前的hash值对newCap取模,这里我们通过位运算来加快运算
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)//如果有8个以上的结点
((TreeNode)e).split(this, newTab, j, oldCap);
else { //如果有节点当只有小于8个,这时候我们就是对链表进行操作,老的每一个桶的位置,都有可能进入新的低位和新的高位
Node loHead = null, loTail = null;//低位的Head
Node hiHead = null, hiTail = null;//高位的head
Node next;
do {
next = e.next;//用next保存链表
if ((e.hash & oldCap) == 0) {//如果当前预算结果得到的是0,那么
//就放在低位,如果当前运算结果是1,就放在高位,两种插入方式都是尾插法
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,putVal
我们回到上面的构造函数,其中有一段代码是将老结点放入新结点的
for (Map.Entry extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
我们深入理解一下putVal这个函数吧
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//当前的hash桶,p表示一个临时结点
Node[] tab; Node p; int n, i;
//如果当前的hash表是空的
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//直接将扩容后的哈希桶的长度赋值给n
if ((p = tab[i = (n - 1) & hash]) == null)//如果当前i的位置是空的,表示没有发生
//hash碰撞,直接构建一个节点,然后将它挂在index的位置就可以了
tab[i] = newNode(hash, key, value, null);
else {//如果发生了hash冲突
Node e; K k;//如果是key相同,那么直接覆盖
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)//如果是红黑树的结构
e = ((TreeNode)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);
//如果追加结点之后,大于了8,就将链表转化成红黑树
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;
}
}
++modCount;//修改modCount
if (++size > threshold)//更新size,并判断是否需要扩容
resize();
afterNodeInsertion(evict);
return null;
}
以上就是hashMap的初始化过程,我们会在(二)中详细的介绍HashMap的增删改查的过程
7,小结
- 运算通过位运算代替,更高效
- 在进行扩容的过程中,记得将老结点的引用置为null,以便垃圾回收
- 取下标 是用 哈希值 与运算 (桶的长度-1) i = (n - 1) & hash。 由于桶的长度是2的n次方,这么做其实是等于 一个模运算。但是效率更高
- 扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。
- 因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位= low位+原哈希桶容量
- 利用哈希值 与运算 旧的容量 ,if ((e.hash & oldCap) == 0),可以得到哈希值去模后,是大于等于oldCap还是小于oldCap,等于0代表小于oldCap,应该存放在低位,否则存放在高位。这里又是一个利用位运算 代替常规运算的高效点
- 如果追加节点后,链表数量》=8,则转化为红黑树
- 插入节点操作时,有一些空实现的函数,用作LinkedHashMap重写使用。
参考自
https://blog.csdn.net/zxt0601/article/details/77413921