HashMap

说说HashMap的工作原理

从一下3点谈起:

  1. 底层数据结构
  2. 方法实现原理,以put方法举例
  3. HashMap中的特定参数等
  4. 不同JDK版本中HashMap实现方式的差异

HashMap底层结构

HashMap的底层结构为数组+链表或红黑树,当链表长度大于等于默认阈值8并且当前HashMap的容量大于等于MIN_TREEIFY_CAPACITY(默认64)的要求,就会把链表转成
红黑树结构;如果后续调整大小,当红黑树的节点小于等于6以后,又会恢复链表结构。

img

JDK1.7和JDK1.8中HashMap的区别

底层结构不同,JDK1.7中并没有红黑树的结构,全部都是链表;

在链表中插入数据的方式不同,JDK1.7中采用的是头插法(并发时,头插法会产生循环链表进而导致死循环的问题),JDK1.8中采用的是尾插法。

扩容的方式不同,JDK1.8中进行了优化;在JDK1.7中,HashMap扩容的时候需要对原数组中的元素进行重新Hash定位在新数组中的位置,在JDK1.8中采用的是原位置不变或者位置变为索引+旧容量大小。因为我使用的是2次幂的扩展,所以元素的位置要么在原位置,要么是在原位置再移动2次幂的位置,这样既省去了重新计算hash值得时间,而且由于新增的1bit是0还是1是随机的,因此resize的过程也均匀地把之前冲突的节点分散到新的bucket了。

为什么Map中的节点超过8个时才转换成红黑树?

在使用分布良好的用户的hashCodes时,很少使用红黑树结构,因为在理想情况下,链表的节点频率遵循泊松分布(意思就是链表各个长度的命中率依次递减),当命中第8次的时候,链表的长度是8,此时的概率仅为0.00000006,小于千万分之一。

当用户自己实现了不好的哈希算法而导致链表过长,影响查询效率,而此时通过转换红黑树结构用来保证极端情况下的查询效率。

HashMap的初始化和加载因子

HashMap的默认初始化大小是16,加载因子是0.75,扩容的阙值就是12(16*0.75),如果进行HashMap初始化的时候传入了自定义容量大小参数size,那么初始化的大小就是正好大于size的2的整数次方,比如传入10,大小就是16,传入30大小就是32。

之所以HashMap初始化的容量一定是2的整数次幂,是为了在计算出Hashcode之后,可以直接与容量大小进行与操作,将Hashcode的高位置0,保留低位然后直接用作数组下标进行访问。

HashMap为什么线程不安全?

首先HashMap就不是为并发操作设计的Map,Java中有专门的线程安全的Map如ConcurrentHashMap;然后HashMap中没有使用锁,有很多++modCount操作都不保证原子性;然后在并发执行put操作的时候,会导致数据丢失等。

HashMap的put方法的具体流程是怎样的?

HashMap是怎么解决哈希冲突的?