HashMap主体
HashMap中的主体就是一个数组,其中数组中的每一个元素就是一个链表:
hashCode()方法
HashMap中的hashcode()方法:
对于Objects.hashCode(key)这个调用,得解释一下,它实际上就是调用key的hashCode方法,Objects类就是一个工具类,Objects中的hashCode源码如下:
要注意区分:Key自带的hashCode()函数和HashMap中的hashCode()函数(即上面这段代码),前者是产生Key对象的hashcode的值,后者是产生hashMap中键值对K-V的hashcode值,不要搞混
equals()方法
对于HashMap中的euqals方法是这样的:
要注意一个误区:我们常说要重写hashcode()方法和equals()方法,不是指重写HashMap中的hashCode()和equals方法,而是重写Key对象的hashCode()和equals()方法
hash()方法
我们关注点应该在**(h=key.hashCode())^(h>>>16)**,这里表达的意思是:
- 首先我们获取Key的hashCode值
- 然后Key的hashCode值右移16位
- 将Key的hashCode值的高16位和低16位进行异或操作,获得Key的hash值
这里有一个问题:为什么还需要专门调用这个hash()方法来对Key的hashcode进行包装然后再使用,而不是直接使用Key的hashcode值呢?
上面这段代码其实叫扰动函数。
Key.hashCode()函数调用的是Key键值类型自带的hash函数,返回int类型的散列值,理论上散列值是一个int型,如果直接拿散列值作为下标访问HashMap的话,前后加起来大概有40亿个空间,完全超越了HashMap的容量大小,内存是放不下那么长的数组的
putVal()方法
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);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
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;
}
}
if (e != null) { // existing mapping for 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;
}
首先判断table是否为空,如果为空的话,就给这个table初始化为一个长度为16的,负载因子为0.75的数组。否则就初始化为之前table长度的两倍
这里就涉及到hashMap的扩容问题:
扩容就是重新计算容量,向HashMap对象里不停的添加元素,当超过数组的容量时,就会扩容,扩容的函数是resize(),后面我们会讲。
接着我们putVal过程,根据数组的长度和key的hash值来寻找在数组的下标位置,如果该位置还没有元素,则直接插入
如果节点和该节点要插入的位置的首元素相同,则将该节点替换掉首元素
如果要插入的位置是一颗红黑树,则进行树的节点插入操作
如果是在数组链表中没有找到,则直接插入链表的尾部,然后判断此时链表长度是否等于8,如果已经到达8,则进行链表转红黑树的判断:
在链表转红黑树的操作中,我们需要注意的是:并不是当链表长度大于8就转成红黑树,我们看转红黑树的函数可以发现,在链表转红黑树的函数中,里面还有一个判断,就是判断数组的容量是否小于64,如果小于,则进行扩容操作,而不是直接转成红黑树,当数组容量大于64,且链表长度大于8时,才进行链表转红黑树操作
最后进行扩容检查:
resize()函数
首先是获取原HashMap表数组的长度,以及数组的临界值
原数组不为空
如果数组长度已经达到最大临界值(231-1),那么直接将数组的临界值设置为最大临界值,然后返回
当数组的长度大于16时,就将原数组的长度和临界值扩容成原来的两倍
当数组为空时,就将数组初始化为容器大小是16,初始临界值为:16*0.75=12
hashMap常见问题
1.HashMap怎么设定初始容量大小
一般如果new HashMap()不传值,默认大小是16,负载因子是0.75,如果自己传入初始大小k,初始大小为大于k的2的整数次方,其中的算法就是让初始值右移1、2、4、8、16位,分别与自己位或,把高位第一个为1的数不断右移,把高位为1的后面全变为1,最后再进行+1操作
2.HashMap的哈希函数怎么设计?为什么要这样设计
HashMap的哈希函数是先拿到key的hashcode,然后让key的hashcode的高16位和低16位进行异或操作
这样设计的原因在于:自己的高半区和低半区做异或,就是为了混合原始哈希值的高位和低位,这样一来混合后的低位掺杂了高位的部分特征,高位的信息也被变相保留下来,而且加大了低位的随机性,减少了哈希碰撞
3.HashMap的1.8版本相比较1.7版本做了哪些优化
- 数组+链表改成了数组+链表或红黑树
- 链表的插入方式从头插法改成了尾插法,简单来说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后
- 扩容的时候1.7需要对原数组中的元素进行重新hash定位再新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小
- 在插入时,1.7先判断是否需要扩容再插入,1.先进行插入再判断是否需要扩容
4.HashMap是线程安全吗
不是,在多线程环境下,1.7版本会产生死循环(因为采用了头插法)、数据丢失、数据覆盖的问题,1.8版本中会有数据覆盖问题
在HashMap中,产生多线程安全问题主要是插入元素的过程,在这个过程中会有两个地方存在安全问题:
- 第一个就是当A线程判断新元素插入的位置时被挂起,此时B线程执行插入操作,按摩当A线程恢复现场的时候,就会将B线程插入的数据覆盖掉
- 第二个就是++size的过程,我们知道,自增操作不是一个原子性操作,那么当两个线程同时执行时,最后的结果只会加1次