Java集合 高频面试题

  • TreeMap 实现类:底层使用红黑树,保证按照添加的 key-value 对进行排序,实现排序遍历。

  • Hashtable 实现类:古老的实现类,它是线程安全的,但效率低;不能存储 null 的 key 和 value。

  • Properties 实现类:常用来处理配置文件,key 和 value 都是 String 类型。

Java中的 List、Set、Map 之类的集合容器中只能存放引用类型,而不能存放类似于 int、double 之类的基本类型。

参考文献:【传送门

2 如何选用集合

===========================================================================

如果我们需要根据键值获取元素值就选用 Map 接口下的集合,当需要排序时就选择 TreeMap,不需要排序时就选择 HashMap,需要保证线程安全就选用 ConcurrentHashMap;

如果我们只需要存放元素时,就选择实现 Collection 接口的集合,此时如果需要保证元素唯一就选择实现 Set 接口的集合,如 TreeSet 或者 HashSet,此时如果需要排序就只选择 TreeSet;

当确定选择实现 Collection 接口的集合后,如果不需要保证元素唯一,就选择实现 List 接⼝的 ArrayList 或 LinkedList 等集合,此时如果需要线程安全的话,就只能选择 Vector,当不需要保证线程安全时,该集合需要频繁的删除和插入,就选择 LinkedList,需要频繁的查找就选择 ArrayList;

3 扩容机制

=========================================================================

3.1 ArrayList 的扩容机制


ArrayList 底层的数据结构是动态数组,其扩容情况可以分为两种情况:

第一种情况,当 ArrayList 的容量为 0 时,此时添加元素的话,需要扩容,三种构造方法创建的 ArrayList 在扩容时略有不同:

  1. 无参构造,创建 ArrayList 后容量为 0,添加第一个元素后,容量变为 10,此后若需要扩容,则按照 1.5 倍进行扩容;

  2. 当传容量,参数为 0 或者传列表,列表为空构造时,创建 ArrayList 后容量为 0,添加第一个元素后,容量为 1,此时 ArrayList 是满的,下次添加元素时则按照 1.5 倍进行扩容;

第二种情况,当 ArrayList 的容量大于 0,并且 ArrayList 是满的时,此时添加元素的话,进行正常扩容,每次扩容到原来的 1.5 倍。

ArrayList 没有缩容。无论是remove方法还是clear方法,它们都不会改变现有数组elementData的长度。但是它们都会把相应位置的元素设置为null,以便垃圾收集器回收掉不使用的元素,节省内存。

3.2 LinkedList 的扩容机制


LinkedList 的底层是双向链表,所以不需要主动扩容。ArrayList 的底层是动态数组,所以当数组的内存不足时,需要主动扩容。

3.3 HashMap 的扩容机制


HashMap 的扩容机制分为两种情况:

第一种情况,无参构造时,HashMap 的默认初始容量是16,之后每次扩充,容量变为原来的 2 倍;

第二种情况,有参构造时,HashMap 会根据我们传入的容量计算一个大于等于该容量的最小的 2 的 N 次方,例如传 9,容量为16。之后每次扩充,容量变为原来的 2 倍。

为什么HashMap 的容量大小永远是 2 的 N 次方呢?

**为了能让 HashMap 存取⾼效,尽量减少哈希碰撞,也就是要尽量把数据分配均匀。**我们上⾯也讲过了,Hash 值的范围值-2147483648到2147483647,前后加起来⼤概40亿的映射空间,只要哈希函数映射得⽐较均匀松散,⼀般应⽤是很难出现碰撞的。但问题是⼀个40亿⻓度的数组,内存是放不下的。所以这个散列值是不能直接拿来⽤的。⽤之前还要先做对数组的⻓度取模运算,得到的余数才能⽤来要存放的位置也就是对应的数组下标。

我们⾸先可能会想到采⽤ % 取余的操作来实现。但是,重点来了:“如果除数是2的幂次,那么取余(%)操作则等价于与其被除数减⼀的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次⽅;)。” 并且 采⽤⼆进制位操作 &,相对于 % 能够提⾼运算效率,这就解释了 HashMap 的⻓度为什么是 2 的幂次⽅。

4 HashMap 的相关问题?

===================================================================================

4.1 HashMap 的底层实现


JDK1.8 之前

此时 HashMap 的底层实现是数组+链表的形式,HashMap 通过 key 的 hashCode() 经过扰动函数处理过后得到 hash 值,然后利用公式((n - 1) & hash)判断当前元素存放的位置,如果当前位置存在元素的话,就判断该元素与要存⼊的元素的 hash 值以及 key 值是否相同,如果均相同则直接覆盖,如果值不同,则通过拉链法来解决哈希冲突。

所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建⼀个链表数组,数组中每⼀格就是⼀个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

JDK1.8 及之后

对于插入,相比于之前的版本, JDK1.8 在解决哈希冲突时有了较⼤的变化,当链表长度大于阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间。

对于移除,当同一个索引位置的节点在移除后小于 6 个,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点。

4.2 为什么要改成“数组+链表+红黑树”


主要是为了提升在 hash 冲突严重时(链表过长)的查找性能,使用链表的查找性能是 O(n)(线性级),而使用红黑树是 O(logn)(对数级)。

4.3 面试回答


请问 HashMap 的底层实现是怎么样的?

在 JDK1.8 之前的话,HashMap 的底层是通过数组+链表的形式来实现的,在 JDK1.8 及之后的话,则是通过数组+链表/红黑树的形式来实现,这两者的区别在于,当哈希冲突严重时,后者会将链表转换成红黑树来存储,从而提升查找性能。

2 什么是哈希冲突

因为 HashMap 在存储数据时是根据键的哈希值所对应的位置来存放的,然后可能存在键不同,对应的位置相同的情况,这种情况便是哈希冲突,在实际中,常采用拉链法来解决,拉链法指的是将链表和数组相结合,数组中每⼀格就是⼀个链表,若遇到哈希冲突,则将冲突的值加到链表后即可。

3 什么是红黑树,红黑树如何进行自调整,与哈希表的区别

红黑树是一种可以自平衡的二叉查找树。(两头乌,路径上不能有两个连续的红色节点)

红黑树通过变色、左旋转和右旋转来保持平衡。

与哈希表的区别:

  1. 在增改查性能上,红黑树的性能是稳定且高效的,而哈希表当哈希碰撞严重时,时间复杂度会达到线性级;

  2. 红黑树是有序的,哈希表是无序的;

  3. 相对来说,红黑树占用的内存更小。

5 HashSet 的相关问题

==================================================================================

5.1 HashSet 的底层实现


HashSet 的底层实际上是通过 HashMap 来实现的,除了实现 Set 接口所规范的方法外,其它方法都是直接调用 HashMap,HashSet 将对应元素存放在 HashMap 的键上,与此相对应的值存放了一个静态的 final 修饰的 Object 对象。

5.2 HashSet 如何检查重复


当把对象加入到 HashSet 中时,HashSet 会比较该对象的哈希值是否和 HashSet 中已经存在的对象的哈希值相等,如果不相等则直接加入;如果相等,则调用 equals() 方法来检查这两个哈希值相等的对象是否真的相同,如果两者相同,HashSet 就不会让其加⼊操作成功。

6 ConcurrentHashMap 的相关问题

============================================================================================

6.1 ConcurrentHashMap 的底层具体实现


JDK1.8 之前

请添加图片描述

⾸先将数据分为⼀段⼀段的存储,然后给每⼀段数据配⼀把锁,当⼀个线程占⽤锁访问其中⼀个段数据时,其他段的数据也能被其他线程访问。

ConcurrentHashMap 是由 Segment 数组 + HashEntry 数组 + 链表组成。

⼀个 ConcurrentHashMap ⾥包含⼀个 Segment 数组,Segment 每一格包含着一个 HashEntry 数组, HashEntry 数组的每一格则包含一条链表,每个 Segment 元素守护着 HashEntry 数组⾥的元素,当对 HashEntry 数组的数据进⾏修改时,必须⾸先获得对应的 Segment 的锁。

JDK1.8 之后

请添加图片描述

ConcurrentHashMap 取消了 Segment 分段锁,采⽤ CAS(Compare And Swap,比较并交换) 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。在链表⻓度超过⼀定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红⿊树(寻址时间复杂度为 O(log(N)))

synchronized 只锁定当前链表或红黑树的首节点,这样只要 hash 不冲突,就不会产生并发,效率⼜提升 N 倍。

6.2 面试回答


1 ConcurrentHashMap 相对于 HashMap 做了哪些优化

ConcurrentHashMap 相对于 HashMap 是线程安全的,它采用了分段锁的形式来保证了线程安全;

JDK1.8 之前的话,它是由 Segment 数组 + HashEntry 数组 + 链表组成的,其中 Segment 数组的每一格包含着一个 HashEntry 数组, HashEntry 数组的每一格则包含一条链表,当对链表中的数据进行修改时,必须首先获得对应的 Segment 的锁。

在 JDK1.8 及之后,取消了 Segment 分段锁,采用 CAS 和同步的方式来保证并发安全。数据结构跟 HashMap 类似,也是采用数组+链表/红黑树,同时同步关键字只锁定当前链表或红黑树的首节点,这样只要 hash 不冲突,就不会产生并发。

7 相似集合的异同

============================================================================

7.1 ArrayList 和 LinkedList 的异同


  1. 底层数据结构: Arraylist 底层使⽤的是 Object 数组; LinkedList 底层使⽤的是双向链表;

  2. 线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;

  3. 是否支持快速随机访问: LinkedList 不⽀持⾼效的随机元素访问,⽽ ArrayList ⽀持。快速随机访问就是通过元素的序号快速获取元素对象(对应于 get(int index) ⽅法);

  4. 插入和删除是否受元素位置的影响: ① ArrayList 采⽤数组存储,所以插⼊和删除元素的时间复杂度受元素位置的影响。 ⽐如:执⾏ add(E e) ⽅法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插⼊和删除元素的话( add(int index, E element) )时间复杂度就为 O(n-i)。因为在进⾏上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执⾏向后位/向前移⼀位的操作。 ② LinkedList 采⽤链表存储,如果是要在指定位置 i 插⼊和删除元素的话( (add(int index, E element) ) 时间复杂度近似为 o(n)) 因为需要先移动到指定位置再插⼊;(总的来说,查找遍历用 ArrayList,增加删除用 LinkedList

  5. 内存空间占用: ArrayList 的空间浪费主要体现在在列表的结尾会预留⼀定的容量空间(因为扩容时数组的大小不太可能是刚刚好的),⽽ LinkedList 的空间花费则体现在它的每⼀个元素都比 ArrayList 需要更多的空间(因为要存放前驱、后继以及数据)。

7.2 ArrayList 和 Vector 的异同


  • ArrayList 是 List 的主要实现类,底层使⽤ Object[ ] 存储,线程不安全 ;

  • Vector 是 List 的古老实现类,底层使⽤ Object[ ] 存储,线程安全的,因此效率低;

线程安全是多线程编程时的计算机程序代码中的一个概念,在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。

线程的安全是以牺牲效率为代价的,所谓线程安全就是多了个加锁、解锁的操作,比如100亿个操作中都要加锁和解锁,线程是安全了,但效率就下降了。而有些软件是以效率为主的,为了提高效率,就少了加锁,解锁的操作,虽然容易出现并发访问问题,但效率却提高了。

7.3 HashMap 和 Hashtable 的异同


  1. 线程是否安全: HashMap 是非线程安全的, HashTable 是线程安全的,因为 HashTable 内部的⽅法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使⽤ ConcurrentHashMap 吧!);

分享

首先分享一份学习大纲,内容较多,涵盖了互联网行业所有的流行以及核心技术,以截图形式分享:

(亿级流量性能调优实战+一线大厂分布式实战+架构师筑基必备技能+设计思想开源框架解读+性能直线提升架构技术+高效存储让项目性能起飞+分布式扩展到微服务架构…实在是太多了)

其次分享一些技术知识,以截图形式分享一部分:

Tomcat架构解析:

算法训练+高分宝典:

Spring Cloud+Docker微服务实战:

最后分享一波面试资料:

切莫死记硬背,小心面试官直接让你出门右拐

1000道互联网Java面试题:

Java高级架构面试知识整理:

知识,以截图形式分享一部分:**

Tomcat架构解析:

[外链图片转存中…(img-KQE4chgS-1714395216654)]

算法训练+高分宝典:

[外链图片转存中…(img-up4HeS3l-1714395216655)]

Spring Cloud+Docker微服务实战:

[外链图片转存中…(img-5a8d668S-1714395216655)]

最后分享一波面试资料:

切莫死记硬背,小心面试官直接让你出门右拐

1000道互联网Java面试题:

[外链图片转存中…(img-iXH1QTeZ-1714395216656)]

Java高级架构面试知识整理:

[外链图片转存中…(img-f2aXs51O-1714395216656)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值