HashMap面试题

提到HashMap,我们都知道这是老生常谈的面试题,但是你对它到底了解有多深,怎样回答才会让面试官对你心服口服?

  1. HashMap你常用的吧,你跟我讲讲它的数据结构
    HashMap的数据结构是数组+链表,在JDK1.8之后,是数组+链表/红黑树

  2. 那你可以跟我讲讲HashMap插入数据(put)是怎么实现的吗?
    (1) 首先判断数组是否为空,为空则进行初始化
    (2) 不为空,计算key的hash值,通过(n-1)& hash计算应当存放的下标
    (3) 查看下标是否存在数据,没有数据就构造一个node节点存放在这个下标位置中
    (4) 如果存在,说明发生了哈希冲突(两个节点的key的hash值一样),这时继续判断key是否相同,如果相同,就用新的value替换旧的value,
    (5) 如果不相等,就判断当前节点类型是不是树型节点,如果是树型节点,就创造树型节点插入红黑树中
    (6) 如果不是树型节点,就创建普通节点加入链表中,判断链表长度是否大于8,如果大于8,链表就会转换成红黑树
    (7) 插入完成后判断当前节点数是否大于阈值,如果大于则开始扩容为原数组的2倍

  3. 等下,我刚刚听到你说它会进行初始化,还有阈值,你能跟我讲一下它是如何设定初始容量大小的嘛?
    一般情况下,new HashMap()里面不传数值,容量默认大小就是16,负载因子是0.75,如果自己传入了一个k值,那么他的初始容量=大于k的2的整数次方的最小值,列如K=10,那么初始容量就是16(2的4次方),如果K=17,那么初始容量就是32(2的5次方)。

  4. 你刚刚说的那个判断节点数大于阈值,这个阈值是多少,又为什么是这个数值而不是其他数值呢?
    这个阈值是8,红黑树转链表的阈值是6;因为只有长度N>=7的时候,红黑树的平均查找长度lgN才会小于链表的平均查找长度N/2,这个可以画函数图来确定,lgN与N/2的交点处N约为6.64。设置成8是为了防止出现频繁的链表与红黑树的转换,当大于8的时候链表转红黑树,小于6的时候红黑树转链表,中间这一段作为缓冲。

  5. HashMap里有一个hash函数,你知道他是怎么设计的吗?
    hash函数先拿到key的hashcode值,是32位的int值,然后让hashcode的高16位和低16位进行异或操作。

  6. 那你知道为什么要这么设计吗?
    (1)尽可能降低hash碰撞,越分散越好
    (2)算法一定要尽可能的高效,因为这是高频操作,所以采用位运算

  7. 那为什么采用hashcode的高16位和低16位异或可以降低hash碰撞?hash函数直接用key的hashcode不行吗?
    因为key.hashcode()方法调用的是key键值类型自带的hash函数,返回int类型散列值。int值的范围是前后加起来大约是40亿的映射空间。只要是hash函数映射的比较均匀松散,一般应用是很难出现碰撞的。但是40亿长度的数组,内存肯定放不下。如果HashMap数组的初始大小才16,用之前还需要先对数组的长度进行模运算,得到的余数才能用来访问数组下标。所以用位运算,位运算比模运算快。这里也刚好解释了为什么HashMap的数组长度要取2的整数幂。因为这样,(数组长度-1)正好相当于一个“低位掩码”。“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。

  8. 为什么它使用红黑树不使用二叉树?
    红黑树牺牲了一些查找性能,但是本身并不是完全平衡的二叉树。因此插入删除效率高,二叉树是相反的。

  9. 你之前提到了负载因子,你知道它有什么作用吗?
    负载因子表示HashMap的拥挤程度,影响到hash操作到同一个数组位置的概率。默认的负载因子是0.75,当hashmap里容纳的元素达到数组长度的75%时,就会扩容,在HashMap的构造器中可以定制负载因子。

  10. 你现在可以给我讲讲get方法的过程吗?
    计算出key的hashcode,根据hashcode定位该元素所在桶的下标,若桶为空,则返回null,不为空,遍历Entry对象链表,直到找到元素,没找到就返回null。

  11. HashMap如何解决冲突?
    (1) 链地址法:将冲突的元素存入数组后面的链表中,hashMap中使用的方法就是链地址法,也就是数组+单链表
    (2) 再哈希:同时构造多个不同的hash函数,第一个冲突就使用第二个,以此类推
    (3) 建立公共溢出区:将冲突的元素存入数组后面的链表中,hashMap中使用的方法就是链地址法,也就是数组+单链表。
    (4) 开放地址法:从发生冲突的单元起,按照一定的顺序从哈希表中找出一个空白单元,然后把冲突元素存入该单元的方法

  12. JDK1.8对HashMap还做了哪些优化?
    (1) 链表的插入方式从头插入法改成了尾插入法,说白了就是,如果数组位置上已经有元素,JDK1.7将新元素放到数组中,原始节点作为新节点的后继节点,JDK1.8遍历链表,将元素放置到链表的最后(JDK1.8使用尾插法是因为1.7头插法扩容时,头插法会使链表发生转移,多线程环境下会产生环)。
    (2) 在插入时,JDK1.7要先判断是否需要扩容,再插入,JDK1.8先进行插入,插入完成再判断是否需要扩容。
    (3) 扩容的时候JDK1.7需要对原数组中的元素进行重新hash定位在新数组的位置,JDK1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小(由于扩容时扩大为原数组大小的2倍,用于计算数组位置的掩码仅仅只是高位多了一个1)。

  13. 那问了这么多,HashMap是线程安全的吗?为什么?
    线程不安全,多线程环境下,JDK1.7会产生死循环,导致数据丢失与覆盖;JDK1.8会出现数据覆盖。

  14. 那我现在想要在多线程环境下使用,怎么办呢?
    请看《线程安全集合之ConcurrentHashMap》讲解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值