实现与map接口,java中用于存储key-value键值对的数据。
在jdk1.7及之前版本的hashMap底层使用的是:Entry数组+链表
在jdk1.8及以后版本的hashMap底层使用的是:Node数组+链表+红黑树
重要成员变量
new HashMap<>()的时候不指定容量大小 默认是16,采用<< 位运算来设置容量大小;但是如果指定了容量是15这种,它底层会做一次修改,会改成比15大的,必须要是2的指数次幂的数,会变成16。
为什么容量要设置成2的指数次幂呢?
因为hashMap定位到数组index用的是&位运算:hashcode & (length-1),这个&运算结果和算hashcode%length的取模运结果是一样的,但是&位运算效率高出10倍,因为&位运算是最接近机器语言运算的,所以效率非常高。这样也能保证计算出来的数组index能够均匀散列分布,减少哈希碰撞。
为什么要用hashcode & (length-1)计算,为什么是length-1?
因为结果和hashcode%length相同,还有如果是hashcode & length 假设长度是16,那么&出来的结果是0或16,这种就很容易导致散列不均匀,hash冲突。
========================================================================
hashMap通过hashcode & (length-1)计算数组index,能够快速定位,所以时间复杂度是O(1),到那时获取值得时候,需要到链表去逐一去获取,所以获取值得时间复杂度是O(n)。
计算数组索引index的源码:
hashMap的扩容加载因子:
有一个加载因子loadfactor,DEFAULT_LOAD_FACTOR = 0.75f;//默认加载因子
hashMap的put过程:
1、获取key的hashcode值,
2、根据hashcode & (length-1)计算出数组中所在的index下标,
3、判断index位置是否有元素,如果没有则直接存放,
4、如果index位置存在元素,则调用equals方法判断对象是否相同,若对象相同则替换原来的value,如果不同,则发生了hash碰撞,jdk1.7采用头部插入法,jdk1.8尾部插入,把新元素next指向链表中的元素,新元素存放数组中.
hashMap的扩容:
在jdk1.7的时候,如果容量达到加载因子的大小的时候,会扩容,扩容大小是之前的2倍,满足2的指数次幂;然后调用transfer方法进行扩容,遍历原来的数组,循环遍历链表,根据hashcode重新计算新数组所在的index,然后采用头插法把链表数据写入新数组中。但是整个链表的顺序倒序了。
但是jdk1.7的扩容有个问题,单线程问题不大,但是多线程同时扩容操作的时候容易导致链表成环,导致put会出现死锁问题。
在jdk1.8开始扩容代码都改了,调用resize方法扩容,定义了高低位头尾指针,loHead,loTail,HiHead,hiTail,for遍历旧数组根据index下标获取元素,判断元素的hashcode & 原数组大小位运算,如果元素是0则用低位指针,如果hashcode & 原数组大小结果是16, 用高位指针;那么就形成了一条高位链表一条低位链表,如果是低位链表,会把低位链表存放在index相同的新数组中,如果是高位链表,会存在index+旧数组长度的位置上。
这样就避免了1.7版本的链表环死锁问题。
====================================================================
在jdk1.8的hashMap还引入了红黑树,红黑树查询时间复杂度O(logn),链表转红黑树的条件:
1、数组容量必须>=64 (MIN_TREEIFY_CAPACITY )的时候链表会转红黑树,否则有限扩容;
2、只有链表过长,阈值设置TREEIFY_THRESHOLD = 8,当链表长度大于8,会转红黑树。
为什么阈值设置TREEIFY_THRESHOLD = 8?
根据加载因子 0.75 和 泊松分布计算(exp(-0.5) * pow(0.5, k),链表的大小达到8的概率很小。
==============================================================
HashMap不是线程安全的,在多线程环境下会存在线程安全问题
jdk1.7,多线程并发扩容会存在链表环,导致死锁问题。
jdk1.8,多线程并发执行put操作的时候会发生数据覆盖的情况。(两个线程同时put操作,如果hashcode相同就会出现hash碰撞,然后写入链表的时候,可能会导致第二个线程覆盖第一个线程的数据)