Java面试题(七):集合类面试题

HashMap面试题

1. HashMap

1.1. 哈希值、哈希表、哈希函数

  • 哈希值:通过一定的散列算法,把一个不固定长度的输入,转换为固定长度的输出,结果我们称之为哈希值(hash)。map中,hash就是一个int值。
    在这里插入图片描述
    在这里插入图片描述
    在JDK1.7之前,HashMap采用数组+链表的形式存储数据,查找的时候,先通过hashCode查找数组下标,要是数组下标的位置有链表,那就再遍历链表通过equals方法查找,要是hash冲突很严重(链表很长),这样是很耗时的。

1.2. HashMap的长度

  • 问:HashMap的长度规则
    答:HashMap的长度规定都是2的幂次方数。
  • 问:为什么,这样有什么好处?
    答:1. 充分利用数组空间
    在这里插入图片描述
    在这里插入图片描述
  1. 在扩容时,数组的数据迁移不用rehash
    在这里插入图片描述

1.3. 红黑树

在JDK1.7之前,HashMap的哈希表是由 数组+链表 构成
在JDK1.8后,HashMap的哈希表是由 数组 + 链表 + 红黑树 构成
在这里插入图片描述
当数组长度大于64,并且桶内元素的数量大于8的时候,才会扩树化,否则当桶内元素太多的时候会扩容

在这里插入图片描述
面试题

  1. 为什么当桶节点数大于8才转化为红黑树?
    我们来看看官方的解释
    在这里插入图片描述
    我们通过自己的语言来整理一下
    在这里插入图片描述
    在这里插入图片描述

1.4. 负载因子

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.5. put和get

  1. put方法
    在这里插入图片描述
  2. get方法

1、这里利用key的hashcode方法和equals方法,所以在使用HashMap的时候,如果使用对象作为key,覆写key的hashcode和equals方法,不然可能出put到HashMap的时候,成功了,但是get的时候却没有找到数据
2、如果key hash冲突太多,会造成链表过长,在链表中查找元素的时候,会比较慢

1.6. 扩容(resize)

1.6.1. 扩容时机

在这里插入图片描述

1.6.2. 扩容方式
  1. JDK1.8的扩容方式
    在这里插入图片描述
    在这里插入图片描述
  • 结论:在JDK1.8后的这种扩容方式,不需要重新hash,数据在新数组中的位置要么等于旧数组位置,要么等于 旧数组长度 + 旧数组位置
    在这里插入图片描述
  1. JDK1.7和JDK1.8的扩容方式对比
    在这里插入图片描述

1.7. 为什么选择红黑树?而不选择avl 树

  • AVL树和红黑树有几点比较和区别:
  1. AVL树是更加严格的平衡,因此可以提供更快的查找速度,一般读取查找密集型任务,适用AVL树。
  2. 红黑树更适合于插入修改密集型任务。
  3. 通常,AVL树的旋转比红黑树的旋转更加难以平衡和调试。
  • 总结:
  1. AVL以及红黑树是高度平衡的树数据结构。它们非常相似,真正的区别在于在任何添加/删除操作时完成的旋转操作次数。
  2. 两种实现都缩放为a O(lg N),其中N是叶子的数量,但实际上AVL树在查找密集型任务上更快:利用更好的平衡,树遍历平均更短。另一方面,插入和删除方面,AVL树速度较慢:需要更高的旋转次数才能在修改时正确地重新平衡数据结构。
  3. 在AVL树中,从根到任何叶子的最短路径和最长路径之间的差异最多为1。在红黑树中,差异可以是2倍。
  4. 两个都给O(log n)查找,但平衡AVL树可能需要O(log n)旋转,而红黑树将需要最多两次旋转使其达到平衡(尽管可能需要检查O(log n)节点以确定旋转的位置)。旋转本身是O(1)操作,因为你只是移动指针。

1.8. 为什么在链表变成红黑树是8, 但是红黑树变链表是6

TreeNodes(红黑树)占用空间是普通Nodes(链表)的两倍,为了时间和空间的权衡。节点的分布频率会遵循泊松分布,链表长度达到8个元素的概率为0.00000006,几乎是不可能事件.为什么转化为红黑树的阈值8和转化为链表的阈值6不一样,是为了避免频繁来回转化。

1.9. 哈希冲突的解决方法

1)开放定址法:当冲突发生时,使用某种探查(亦称探测)技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定 的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探查到开放的 地址则表明表中无待查的关键字,即查找失败。
2) 再哈希法:同时构造多个不同的哈希函数。
3)链地址法:将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
4)建立公共溢出区:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

1.10. HashMap为什么线程不安全

  1. 多线程下扩容死循环
    JDK1.7 中的 HashMap 使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。因此,JDK1.8 使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题。
  2. 多线程的 put 可能导致元素的丢失
    多线程同时执行 put 操作,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。此问题在 JDK 1.7 和 JDK 1.8 中都存在。

2. ConcurrentHashMap

2.1. Sizectl

当时我们new ConcurrentHashMap(32)的时候,真实容量其实是64,这个容量在底层被计算出来之后赋值给了Sizectl
在这里插入图片描述

2.2. ConcurrentHashMap1.7和1.8的区别

首先从数据结构上来看,JDK1.8摒弃了JDK1.7中的Seqment数组的概念,直接把哈希表的每一个位置当作一个分段。
JDK1.7是若干个下标有一个Seqment,一个Seqment有一把锁
JDK1.8是每一个下标都有一把锁
在这里插入图片描述

2.3. ConcurrentHashMap 1.8 源码解析

ConcurrentHashMap 1.8 源码解析

3. ArrayList

3.1. ArrayList扩容机制

ArrayList底层是数组实现的,初始化容量是10,当需要扩容的时候,先创建一个容量为 10*1.5 + 1 = 16 的数组,再把原来旧数组的10个值给新数组(Arrays.copyOf(old,new)),再用新数组覆盖掉旧数组,再添加值。
扩容:新数组容量 = (旧数组容量 * 3)/ 2 + 1

  • 为什么要+1?
    当数组容量设置为1的时候,若不+1,(3*1)/2的扩容这个值,永远是1,会导致扩容失败。
3.1.2. 源码分析

我们结合源码来看一下
在这里插入图片描述

3.2. ArrayList特点

  1. 删除和插入效率低
  2. 查找效率高

4. 为什么重写equals一定要重写hashCode

相等: 如果A和B相等,则A.equals(B)为true:如果A.equals(B)为true,则A和B相等;

相同:如果A和B相同,则A.hashCode() == B.hashCode(); 如果A.hashCode() == B.hashCode(); 则A和B相同

此问题的解释就会是:

如果只重写equals()方法,就会出现相等的两个对象不相同, 如果只重写hashCode()方法,就会出现相同的两个对象不相等

当调用Map或者set的时候,就会出现问题,因为map是根据key的hashCode在寻找index,再根据equals来对比

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

若能绽放光丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值