简易讨伐HashMap

1 谈谈你对HashMap的认识吧。

HashMap底层由 数组+链表/红黑树 实现。

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

向HashMap容器中插入Entry对象时,会先使用对象的key调用哈希函数计算出一个hash值,然后使用这个hash值和[数组长度-1] 做位与运算得到一个数组下标。如果数组该下标的位置为空,就插入,如果下标位置上已有其它Entry对象,说明发生了hash冲突(也叫hash碰撞),就把要插入的Entry对象和该位置上的其它Entry对象通过next属性连接起来形成链表。

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

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

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

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

1.1.1 为什么不直接用红黑树呢?

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

    当链表长度很小的时候,即使不转换成红黑树,查找速度就已经够用了。

    链表转换成红黑树会消耗资源。

    链表转换成红黑树之后,会占用较大的空间。

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

1.2 链表的插入方式是头插入还是尾插入?

JDK1.8以后,由原来的头插入改成了尾插入。

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

1.3 红黑树的数据结构是什么样的?

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

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

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

调平衡操作包括左旋、右旋和变色。红黑树的任何不平衡问题都能在三次旋转之内解决。

1.4 遍历HashMap的时间复杂度是多少?

HashMap根据key查找数组下标的时间复杂度为O(1),不影响整体遍历的时间复杂度。

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

1.5 哈希函数是怎么计算hash值的?

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

1.5.1 为什么要进行hashCode的高低位异或运算?

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

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

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

HashMap的数组长度一定是2的n次幂,所以 [数组长度-1] 换算成二进制的每一位都是1。反过来,这也就是为什么HashMap的数组长度必须是2的n次幂

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

新建的HashMap容器的容量为16,加载因子默认为0.75。

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

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

JDK1.8及之后,只需要将原数组内 各元素的hash值与原数组长度 做位与运算,若结果为0,元素位置不变,若结果不为0,元素位置的下标变为 原位置下标+原数组长度。这样经过数组扩容后,元素要么在原位置,要么在 原位置向右移动原数组长度 的位置。

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

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

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

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

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

两种方案。

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

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

3.1 ConcurrentHashMap是怎么保证线程安全的?

JDK1.7及以前:

ConcurrentHashMap使用分段式锁来保证线程安全,可以理解为把一个Map容器拆成n个Segment容器,每个Segment容器分配一把锁,同一时间只允许一个线程持有这把锁。但是宏观上,多个线程可以同时访问这个Map容器。

ConcurrentHashMap是由一个Segment数组和多个HashEntry数组+链表组成的,Segment数组中的每一个元素都是Segment容器,存储一个HashEntry数组+链表。当线程执行添加或删除时,只锁住对应的Segment容器,不影响对其它Segment容器的操作。

JDK1.8及以后:

不再使用Segment+HashEntry+链表的结构了,改为像HashMap一样的数组+链表/红黑树的结构。对链表/红黑树的头/根结点加synchronized锁,在同一时间,只能有一个线程对该链表/红黑树进行操作。

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

预定义存储空间,减少HashMap扩容次数。预定义存储空间 = 数据量/加载因子 + 1。

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

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值