论道HashMap

目录

——谈谈你对HashMap的认识吧。

——jdk1.8为什么要引入红黑树?

——遍历链表和红黑树的时间复杂度各是多少?

——来手写一个红黑树。

——HashMap什么时候用链表,什么时候用红黑树?

——哈希函数具体是怎么实现的?

——为什么要进行hashCode的高低位异或操作?

——怎么使用hash值计算数组下标?

——为什么jdk1.8中链表的插入方式要改为尾插入?

——你知道HashMap的扩容机制吗?

——为什么加载因子默认是0.75?

——再问你一些简单的问题。

——HashMap的key可以为null吗?

——那key为null的情况下,会把null存到什么位置?

——HashMap的key是有序存放的吗?

——如果我想有序存放要怎么办?

——如果要用HashMap存一万条数据,怎么做能提高效率?

——如何在高并发的情况下使用HashMap?


——谈谈你对HashMap的认识吧。

HashMap底层由 数组(也叫位桶)+链表/红黑树 实现,其中红黑树是jdk1.8引入的。【——jdk1.8为什么要引入红黑树?】【——HashMap什么时候用链表,什么时候用红黑树?】

HashMap首先维护了一个数组,数组中的每个元素都是一个Entry对象,每个Entry对象包含四个属性:[key、value、next、hash]。

其中key、value就是插入键值对的键和值,next是对另一个Entry对象的引用,默认为null,hash是这个Entry对象的哈希值。

向HashMap容器中插入Entry对象时,会先使用对象的key通过哈希函数得到一个hash值,然后使用hash值计算得到一个数组下标。【——哈希函数具体是怎么实现的?】【——怎么使用hash值计算数组下标?】如果得到的数组下标的位置为空,就插入;如果位置上已有其它Entry对象,说明发生了hash冲突(也叫hash碰撞),当发生hash冲突时,就把要插入的Entry对象和该位置上的其它Entry对象通过next属性连接起来形成链表。

jdk1.7及之前,采用头插入方式,即 将新来Entry对象的next属性设为对链表上首个Entry对象的引用。jdk1.8及之后,改为尾插入方式。【——为什么jdk1.8中链表的插入方式要改为尾插入?】

——jdk1.8为什么要引入红黑树?

jdk1.7及之前,HashMap的结构是 数组+链表。链表的优点是添加、删除元素很快,但查询效率比较低。引入红黑树是为了提高HashMap的查询效率

红黑树是 平衡二叉查找树。

二叉查找树的规则是任意结点的左子树(如果有)上的所有结点的值均小于该结点的值,右子树(如果有)上的所有结点的值均大于该结点的值。

红黑树在二叉查找树的基础上做了平衡,保证每个结点的左子树和右子树的高度差最大为2,如果超过了就进行调平衡。

调平衡操作包括左旋、右旋和变色。

红黑树的任何不平衡问题都能在三次旋转之内解决。

红黑树每个结点的左右子树的高度差最大为2,说明红黑树不是严格的平衡二叉树(AVL)。

【实际上红黑树每个结点的左右子树上的黑色结点的高度差永远为0。】

为什么不用AVL树而采用红黑树?

AVL树调平衡的代价比较大。

——遍历链表和红黑树的时间复杂度各是多少?

遍历链表的时间复杂度是O(n),遍历红黑树的时间复杂度是O(logn)。

HashMap根据key查找的时间复杂度只取决于桶里面的数据结构。

——来手写一个红黑树。

戳下面网址练习:

https://rbtree.phpisfuture.com/

——HashMap什么时候用链表,什么时候用红黑树?

数组上某个下标位置上的结点数目增加到8个时,链表转换成红黑树;之后,结点数目降到6个时,红黑树转换成链表。

HashMap是为了提升查询效率才采用红黑树结构,但是,

  • 当链表长度很小的时候,即使不转换成红黑树,查找速度就已经够用了;
  • 链表转换成红黑树会消耗资源;
  • 链表转换成红黑树之后,会占用较大的空间。

所以能用链表满足查询效率需求,就尽量避免转换成红黑树。

链表转换成红黑树的阈值为8,是因为在理想情况下,所有结点在数组内遵循泊松分布,在数组的某个下标上链表长度达到8的概率微乎其微,这就保证了绝大部分情况下,链表不会转换成红黑树。

红黑树转换成链表的阈值为6,是为了避免链表和红黑树之间频繁地来回转换。

——哈希函数具体是怎么实现的?

向HashMap容器中插入Entry对象时,会先使用对象的key调用一个native方法hashCode(),得到一个int类型的hashCode;然后将(32位的)hashCode右移16位,与hashCode本身做异或运算,得到hash值。

——为什么要进行hashCode的高低位异或操作?

为了让hash值更加不确定,降低hash冲突的概率。

——怎么使用hash值计算数组下标?

使用hash值和 [数组长度-1] 做位与运算,得到一个0到 [数组长度-1] 的数,就是插入位置的下标。

HashMap的数组长度一定是2的n次幂,所以 [数组长度-1] 换算成二进制的每一位都是1。

【反过来,这也就是为什么HashMap的数组长度必须是2的n次幂。】

——为什么jdk1.8中链表的插入方式要改为尾插入?

如果采用头插入方式,在并发场景下,扩容时可能会出现循环链表的情况,采用尾插入方式会避免这一情况发生。

——你知道HashMap的扩容机制吗?

新建的HashMap容器的容量为16,加载因子默认为0.75。【——为什么加载因子默认是0.75?】

HashMap容器中的元素数量>=容量*加载因子 时,HashMap会进行扩容。每次扩容HashMap的容量会扩大一倍(×2)。

jdk1.7及之前,扩容的核心思想是:使用一个容量更大的数组来代替原来的数组,将原数组内的元素拷贝到新数组当中。拷贝过程会遍历原数组内的元素,将元素依次插入到新数组当中。

jdk1.8及之后,只需要将原数组内 各元素的hash值与原数组长度 做位与运算,若结果为0,元素位置不变,若结果不为0,元素位置的下标变为 原位置下标+原数组长度。

经过数组扩容后,元素要么在原位置,要么在 原位置向右移动原数组长度 的位置。

——为什么加载因子默认是0.75?

当 HashMap容器中的元素数量>=容量*加载因子 时,HashMap进行扩容。

可以看出,加载因子越大,HashMap容器中的空间利用率越高,但相应的,hash冲突的概率越高;加载因子越小,空间利用率越低,hash冲突的概率越低。

加载因子默认是0.75是对空间利用率和hash冲突概率的折衷

——再问你一些简单的问题。

——HashMap的key可以为null吗?

可以。

——那key为null的情况下,会把null存到什么位置?

存到数组下标为0的位置。

——HashMap的key是有序存放的吗?

不是。HashMap插入元素时是根据元素的hash值计算插入位置的,所以插入位置是确定的,但不是有序的。

——如果我想有序存放要怎么办?

使用LinkedHashMap或者TreeMap。

LinkedHashMap维护了一个双向链表,记录了结点的插入顺序。

TreeMap维护了一棵红黑树,遍历TreeMap可以按照自然顺序输出所有元素。

——如果要用HashMap存一万条数据,怎么做能提高效率?

预定义存储空间,以减少HashMap扩容次数。

预定义存储空间 = 数据量/加载因子 + 1。

减小负载因子,虽然降低了空间利用率,但是也减少了hash碰撞的概率。

——如何在高并发的情况下使用HashMap?

两种方案。

第一种方案是java.util包提供了包装类Collenctions,里面提供了包装方法synchronizedMap(Hashmap hashMap)。

第二种方案是java.util.concurrent包提供了HashMap的替代类ConcurrentHashMap类。

加油!(ง •_•)ง

猜你喜欢

转载自blog.csdn.net/qq_42082161/article/details/114846075