HashMap的问题,在现在的技术面试中,基本上都会问到,而且这个集合在开发中也会经常使用到,希望通过本文能有所收获。
一、问题分析
1、HashMap的底层数据结构是什么?
2、HashMap中CRUD操作的底层实现原理是什么?
3、HashMap是如何实现扩容的?
4、HashMap的hash冲突是怎样解决的?
5、HashMap为什么线程不安全?
二、认识HashMap
HashMap最早出现在jdk1.2中,一直到jdk1.7都没有太大的变化。但是到了1.8版本的时候,突然进行了一个很大的改动,最显著的就是:变成了数组+链表+红黑树。当链表长度超过阈值8的时候转为红黑树,这样能大大的减少了查找时间。之前版本中都是数组+链表的结构。另外HashMap是非线程安全的,也就是说在多线程中,多个线程对HashMap进行增删改操作的时候,不能保证数据的一致性。
三、深入分析HashMap
1、底层数据结构
如图,是jdk1.7版本中的存储结构图。从图中能看出,首先把元素放在一个个Entry数组里面,然后存放的数据元素越来越多,就出现了链表,对于数组中的每一个元素,都可以有一条链表来表述存储元素。这就是有名的“拉链式”存储方法。
后来,存储的元素越来越多,链表越来越长,当查找一个元素的时候效率不仅没有提高,反而下降了,于是就进行了改进。就是把链表变成了一个适合查找的树形结构,红黑树,所以HashMap的存储数据结构就变成了下面这样:
其实就是把链表结构变成了红黑树,原来的优点是增删操作效率高,现在查找的效率也大大提高了。
注意:只有在链表的长度不小于8且数组长度小于64的时候才会将链表转成红黑树。
什么是红黑树?
红黑树是一个自平衡的二叉查找树,在每个节点增加一个存储位表示节点的颜色,红色或者黑色。通过任意一条从根到子叶的路径上各个节点颜色的限制,红黑树确保没有一条路径会比其他路径长出两倍,因此红黑树是一种弱平衡二叉树。查询效率非常高。
为什么非要等到链表长度大于等于8的时候才转变为红黑树,而不是直接变为红黑树?
1、因为构造红黑树要比构造链表复杂,另外在链表的节点不多的时候,数组+链表+红黑树的结构不一定比数组+链表的结构性能高。
2、HashMap扩容的时候,会造成红黑树不断的进行拆分重组,这是非常耗时的。所以,在链表长度比较长的时候才转变为红黑树,这样才会提高效率。
2、存储元素
在存储一个元素的时候,大多都是使用这种方式:
HashMap(String,Integer) map = new HashMap();
map.put(“张三”,22);
第一个是键,第二个是值,总称为键值对。那么底层是实现原理是咋样的呢?看下图:
第一步:调用put方法传入键值对。
第二步:使用hash算法进行计算。
第三步:根据hash值确定存放的位置,并判断是否和其他键值对发生了冲突。
第四步:如果没有冲突,直接存放到数组中。如果有冲突,要判断此时的数据结构是什么。
第五步:如果是红黑树,就直接插入到红黑树中。如果不是红黑树,要判断链表长度是否大于等于8。
第六步:如果大于8,那就先转为红黑树,然后再插入,如果不大于8,那就直接插入到链表尾部即可。
3、扩容
流程图如下:
扩容比较简单,就是先计算新的hash表容量和新的容量阈值,然后初始化一个新的hash表,将旧的键值对映射到新的hash表里,如果涉及到红黑树,那么映射的过程中还涉及到红黑树的拆分。
4、解决hash冲突
Hash冲突是再计算hash值时候出现了重复,HashMap中计算hash值就是通过hashcode与16异或计算来的。
如图,通过异或运算计算出来的hash比较均匀,不容易出现冲突,但是总有一些例外,一旦出现了冲突现象怎么解决呢?
再数据结构中,处理hash冲常用的办法有:开发定址法、再哈希法、链地址法、建立公共溢出区。而HashMap处理hash冲突的方法就是链地址法。
这种方法的基本思想就是将所有的哈希地址为i的元素构成一个称为同一次链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行,链地址法适用与经常进行插入和删除的情况。
如图,出现冲突的时候,一个接一个拍成一条链就可以了。刚好与hashmap的底层数据结构相呼应。
5、HashMap为什么是非线程安全的?
因为源码里面的方法全部独是非线程安全的,根本找不到synchronized关键字,所以保证不了线程安全。于是出现了ConcurrentHashMap。
上述资料不能面面俱到,如有遗漏,欢迎指正。