【Java面试题】之 集合

文章目录

原文:https://www.yuque.com/unityalvin/baguwen/nw7mkg

1、Java 中都有哪些集合类

常用的有:
在这里插入图片描述
在这里插入图片描述

2、Collection 和 Collections 的区别

● Collection 是一个集合接口。 它提供了对集合对象进行基本操作的通用接口方法。Collection 接口在 Java 中有很多具体的实现。是 List,Set 等的父接口。
● Collections 是一个包装类。 它包含各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,服务于 Java 的Collection 框架。

3、List、Set、Map 的区别

● List(对付顺序的好帮⼿):存储的元素是有序的、可重复的。
● Set(注重独⼀⽆⼆的性质): 存储的元素是⽆序的、不可重复的。
● Map(⽤ key 来搜索的专家): 使⽤键值对(kye-value)存储,key 是⽆序的、不可重复,value 是⽆序的、可重复

4、ArrayList 和 LinkedList 的区别

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

底层数据结构
● ArrayList 底层是 Object 数组,
● LinkedList 底层是双向链表,

插入和删除是否受元素位置的影响
● ArrayList 采用数组存储,所以插⼊和删除元素的时间复杂度受元素位置的影响
● LinkedList采用链表存储,所以对于 add(E e) 方法的插入,删除元素的时间复杂度不受元素位置的影响

是否支持快速随机访问
● ArrayList ⽀持,可直接通过元素序号快速获取元素对象
● LinkedList 不⽀持高效的随机元素访问

内存空间占用
● ArrayList 的空间浪费主要体现在 list 列表的结尾会预留⼀定的容量空间
● LinkedList 的空间浪费则体现在它的每⼀个元素都需要消耗比 ArrayList 更多的空间,因为它除了要存放数据之外,还要额外存放直接前驱以及直接后继

5、ArrayList 初始化容量多少

10,每次扩容1.5倍

6、为什么位运算处理的速度快

因为位运算直接对内存数据进行操作

7、ArrayList 怎么进行扩容

ArrayList 里面有一个元素个数的阈值,如果达到这个阈值了,它会创建一个新数组,通过位运算的方式计算出原先数组容量的 1.5 倍是多少,得出来的值就是新数组的长度,然后再使用Arrays.copyOf()将原先数组的元素迁移到新数组

8、ArrayList 是怎么删除的

● 删除末尾,并不需要迁移
● 删除其他的位置,这个时候也需要搬迁
在这里插入图片描述

9、ArrayList 是怎么修改的

● 修改之前,必须先定位
● 定位 - 查找-ArrayList(数组是一段连续的内存空间,定位会特别快)
在这里插入图片描述

10、ArrayList 与 Vector

ArrayList:线程不安全,效率高,常用
Vector:里面的方法都加了 synchronized ,线程安全,效率低

11、LinkedList 是怎么插入的

LinkedList 有两个指针(first、last),一个指向上一个节点,一个指向下一个节点,每次有新元素插入的时候,都会将指向下一个节点的指针指向最新插入的元素,如果将这个元素插入其他位置,这个时候,就需要调整前后节点的引用指向
在这里插入图片描述
在这里插入图片描述

12、LinkedList 是怎么修改的

根据 first、last 的引用快速找到对应的节点
将节点的值修改
在这里插入图片描述

在这里插入图片描述

13、假如我们要存储 1000 个对象的信息,ArrayList 和LinkedList 哪个更节省内存呢?

ArrayList,因为 LinkedList 除了存数据以外,还要存储两个指针
使用ArrayList可以直接指定其大小为1000

14、如何在双向链表中A和B之间插入C

A.next = C;
C.prev = A;

B.prev = C;
C.next = B;

15、HashSet 的存储原理

HashSet 底层采用的是 HashMap 来实现存储,HashSet 的值作为 HashMap 的 key
HashSet的add方法:
在这里插入图片描述

16、为什么要采用 Hash 算法?有什么优势,解决了什么问题?

当我们往数组放数据的时候,可以采用遍历的方式,来判断数据的唯一性,逐个比较,但是这种方式效率较低,尤其是数据很多的情况下,为了解决这个效率低的问题,我门采用 Hash 算法。

17、HashSet 如何保证保存对象的唯一性?会经历一个什么样的运算过程?

采用 hash 算法,通过计算存储对象的 hashCode,然后再跟数组长度 -1 做位运算,得到我们要存储在数组的哪个下标,如果此时计算的位置没有其他元素,直接存储,不用比较。

但是随着元素的不断添加,就可能出现“哈希冲突”,不同的对象计算出来的 hash 值是相同的,这个时候,就需要用到 equals 方法进行比较,如果 equals 相同,则不插入,不相等,则形成链表,当然以上是 JDK1.7 版本的处理方式。

到了 jdk1.8 则将链表优化成红黑树了,这个优化代表查找速度更快了,因为树是采用二分查找的方式进行查找。

在这里插入图片描述

18、所谓哈希表是一张什么表?

本质是一个数组,而且数组的元素是链表

19、Hashtable 和 HashMap 的区别

线程安全
● Hashtable 中的方法是同步的,线程安全,效率较低
● HashMap 中的方法在默认情况下是非同步的,线程不安全,效率高

允不允许null值
● Hashtable 中,key和value都不允许出现null值,否则会抛出 NPE 异常。
● HashMap 中,null 可以作为键,这样的键只能有一个;HashMap 允许值为 null 。

默认初始容量和扩容机制
● Hashtable 中 hash 数组初始大小是 11,增加的方式是 old * 2 + 1。
● HashMap 中 hash 数组的默认大小是 16,之后每次扩充,容量变为原来的 2 倍

哈希值的使用不同
● Hashtable 直接使用对象的 hashCode。
● HashMap 重新计算 hash 值。

底层数据结构
● JDK1.7 它们的数据结构是一致的,都是数组 + 链表
● JDK1.8 之后, HashMap 在解决哈希冲突时有了较⼤的变化,当链表长度超过 8、会判断数组的⻓度是否小于 64,如果数组的长度小于 64,会进行数组扩容;否则就会将链表转化为红黑树,以减少搜索时间,Hashtable 没有这样的机制

20、简单说一下 HashMap

  1. HashMap 的初始化大小是 16,如果事先知道数据量的大小,建议修改默认初始化大小。 减少扩容次数,提高性能
  2. 最大的装载因子默认是 0.75,当 HashMap 中元素个数达到容量的 0.75 时,就会扩容。 容量是原先的两倍

21、HashMap 的底层

JDK1.8 之前 HashMap 底层是使用的 数组和链表 ,数组是 HashMap 的主体,链表则是为了解决哈希冲突⽽存在的,HashMap 通过 key 的 hashCode 经过 hash() 处理 得到 hash值,然后通过 (数组长度- 1) & hash 判断当前元素存放的位置,如果这个位置存在元素的话,就判断该元素与要存入的元素的 hash 值 以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过链地址法解决冲突。

JDK1.8 对 HashMap 做了进一步的优化,引入了红黑树

  1. 当链表长度超过 8,会判断数组的⻓度是否小于 64,如果数组的长度小于 64,则会进行数组扩容;否则就会将链表转化为红黑树,以减少搜索时间
  2. 当红黑树的节点数量小于 6 时,会将红黑树转换为链表,因为在数据量较小的情况下,红黑树要维护自身平衡,跟链表比性能没有优势。

相⽐于 JDK1.8 的 hash 方法 ,JDK1.7 的 hash 方法性能会稍差⼀点点,毕竟扰动了 4 次。

链地址法:
● 将哈希表的每个单元作为链表的头结点,所有哈希地址为 i 的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。

22、为什么 HashMap 的默认容量设置成 16

HashMap 作为一种数据结构,为了在 put 元素的过程中计算出该元素要存放在 HashMap 中的哪个位置,需要进行 hash 运算

hash 运算的过程其实就是对目标元素的 key 进行 hashcode,再对 Map 的容量进行取模,而 JDK 的工程师为了提升取模的效率,使用位运算代替了取模运算,这就要求 Map 的容量一定得是2的幂。

而作为默认容量,太大和太小都不合适,所以 16 就作为一个比较合适的经验值被采用了。

23、为什么 HashMap 的默认负载因子是 0.75

为了避免哈希碰撞,HashMap 会在其中的元素个数达到临界值的时候,进行扩容,这个临界值(threshold)是由负载因子(loadFactory)与容量(capacity)相乘计算出来的,负载因子代表一个数组可以达到的最大的满的程度,

负载因子太大,比如等于 1,那么就会有很高的哈希冲突的概率,会大大降低查询速度。
负载因子太小,比如等于 0.5,那么频繁扩容,就会大大浪费空间。

所以这个值需要介于 0.5 和 1 之间,根据数学公式推算,这个值在 log(2) 的时候比较合理,再加上 HashMap 为了提升扩容效率,要求 HaspMap 的容量必须是 2 的幂,所以负载因子是 0.75 的话,那么和容量的乘积就可以是一个整数。

一般情况下,不会修改默认负载因子,除非特殊原因,比如明确的知道这个 HaspMap 只存 5 个 kv,并且永远不会改变,那么可以考虑指定负载因子,但是其实这样也不建议使用,因为完全可以通过指定 HaspMap 的容量达到这样的目的。

24、你说一下 ConcurrentHashMap

ConcurrentHashMap 兼顾了线程安全和效率的问题,把数据分段,执行分段锁,核心是把锁的范围变小,这样出现并发冲突的概率就会变小

ConcurrentHashMap 在 JDK1.7 跟 JDK1.8 的实现上存在以下区别

  • 底层数据结构
    ● JDK1.7 采用 Segment 数组 + HashEntry 数组 + 链表 实现
    在这里插入图片描述

● JDK1.8 则采用 Node 数组 + 链表 / 红⿊树 方式,Node 只能⽤于链表的情况,红⿊树的情况需要使⽤ TreeNode 。当冲突链表达到⼀定⻓度时,链表会转换成红⿊树。
在这里插入图片描述

实现线程安全的⽅式
● JDK1.7, ConcurrentHashMap (分段锁)对整个桶数组进⾏了分割分段( Segment ),每⼀把锁只锁容器其中⼀部分数据,多线程访问容器⾥不同数据段的数据,就不会存在锁竞争,提⾼并发访问率。

● JDK1.8,摒弃了 Segment 的概念,⽽是直接⽤ Node 数组+链表+红⿊树 的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。 整个看起来就像是优化过且线程安全的 HashMap,synchronized 只锁定当前链表或红⿊⼆叉树的⾸节点,这样只要 hash 不冲突,就不会产⽣并发,效率⼜提升 N 倍。

25、HashSet、LinkedHashSet、TreeSet 有什么区别

● HashSet 是 Set 接⼝的主要实现类 , HashSet 的底层是 HashMap ,线程不安全的,可以存储 null 值;
● LinkedHashSet 是 HashSet 的⼦类,能够按照添加的顺序遍历;
● TreeSet 底层使⽤红⿊树,能够按照排序后的顺序进⾏遍历,排序的方式有⾃然排序和定制排序。

26、集合底层框架的数据结构总结

List
● ArrayList:Object[] 数组
● Vector:Object[] 数组
● LinkedLsit :双向链表 (JDK1.6 之前为循环链表,JDK1.7 取消了循环)

Set
● HashSet (⽆序,唯⼀):
○ 基于 HashMap 实现的,底层采⽤ HashMap 来保存元素 LinkedHashSet
● LinkedHashSet:
○ 是 HashSet 的⼦类,并且其内部是通过 LinkedHashMap 来实现的。有点类似于 LinkedHashMap,其内部是基于 HashMap 实现⼀样,不过还是有⼀点点区别的
● TreeSet(有序,唯⼀):
○ 红⿊树(⾃平衡的排序⼆叉树)

Map
● HashMap:
○ JDK1.8 之前 HashMap 由数组+链表组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突⽽存在的。JDK1.8 以后在原来结构的基础上又引入了红黑树,当链表长度超过 8,会判断数组的⻓度是否小于 64,如果数组的长度小于 64,则会进行数组扩容;否则就会将链表转化为红黑树,以减少搜索时间
● LinkedHashMap:
○ LinkedHashMap 继承⾃ HashMap,所以它的底层也是数组+链表/红黑树。另外, LinkedHashMap 在上面结构的基础上,增加了⼀条双向链表,使得上面的结构可以保持键值对的插⼊顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
● Hashtable:
○ 数组 + 链表组成的,数组是 Hashtable的主体,链表则是主要为了解决哈希冲突⽽存在的
● TreeMap:
○ 红⿊树(⾃平衡的排序⼆叉树)

27、你是如何选用集合的

主要根据集合的特点来选,比如需要根据键值获取到元素值时就选⽤ Map 接口下的集合,
● 需要排序时选择 TreeMap
● 不需要排序时就选择 HashMap
● 需要保证线程安全就选择 ConcurrentHashMap

如果只需要存放元素值时,就选择实现 Collection 接⼝的集合,
● 需要保证元素唯⼀时选择实现 Set 接⼝的集合⽐如 TreeSet 或 HashSet ,
● 不需要就选择实现 List 接口的,比如 ArrayList 或 LinkedList ,

然后再根据实现这些接口的集合特点来选用

28、HashMap 底层怎么实现

在 JDK1.8 之前使用的是 数组+链表,数组是 HashMap 的主体,链表则是为了解决哈希冲突⽽存在的
在 JDK1.8 进行了优化,在原有结构的基础上,引入了红黑树

29、为什么用红黑树

在 JDK1.8 之前,HashMap 是使用拉链法来解决 hash 冲突的,也就是说它们会使用单向链表来存储相同索引值的元素,这种方式会将 HashMap 的 get 方法的性能从 O(1) 降到 O(n) ,所以为了解决在频繁冲突时 hashMap 性能降低的问题,引入了红黑树来替代链表存储冲突的元素。红黑树时间复杂度:O(log n)

30、红黑树是二叉树吗

31、说说你对红黑树的见解?

  1. 每个节点非红即黑
  2. 根节点总是黑色的
  3. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定)
  4. 每个叶子节点都是黑色的空节点(NIL节点)
  5. 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)

即:根节点为黑,红的子节点为黑,根节点到叶子节点(黑的空节点)经过的黑色节点数目相同

32、HashMap,LinkedHashMap,TreeMap 有什么区别?

LinkedHashMap 保存了记录的插入顺序,在用 Iterator 遍历时,先取到的记录肯定是先插入的;遍历比 HashMap 慢;
TreeMap 实现 SortMap 接口,能够把它保存的记录根据键排序(默认按键值升序排序,也可以指定排序的比较器)

33、HashMap & TreeMap & LinkedHashMap 使用场景?

一般情况下,使用最多的是 HashMap。
HashMap:在 Map 中插入、删除和定位元素时;
TreeMap:在需要按自然顺序或自定义顺序遍历键的情况下;
LinkedHashMap:在需要输出的顺序和输入的顺序相同的情况下。

34、HashMap & ConcurrentHashMap 的区别?

除了加锁,原理上无太大区别。另外,HashMap 的键值对允许有null,但是 ConcurrentHashMap 都不允许。
在这里插入图片描述

35、为什么 ConcurrentHashMap 比 HashTable 效率要高?

HashTable 使用一把锁(锁住整个链表结构)处理并发问题,多个线程竞争一把锁,容易阻塞;

ConcurrentHashMap只锁一部分:
● JDK1.7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一个 HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。
○ 锁粒度则是基于 Segment,包含多个 HashEntry。
● JDK1.8 中使用 CAS + synchronized + Node + 红黑树。
○ 锁粒度:Node(首结点)(实现 Map.Entry)。锁粒度降低了。

36、ConcurrentHashMap 锁机制具体分析

JDK 1.7 中,采用分段锁的机制,实现并发的更新操作,底层采用数组+链表的存储结构,包括两个核心静态内部类 Segment 和 HashEntry。
● Segment 继承 ReentrantLock(重入锁) 用来充当锁的角色,每个 Segment 对象守护每个散列映射表的若干个桶;
● HashEntry 用来封装映射表的键值对;
每个桶是由若干个 HashEntry 对象链接起来的链表

JDK1.8 中,采用Node + CAS + Synchronized来保证并发安全。取消类 Segment,直接用 table 数组存储键值对;当 HashEntry 对象组成的链表长度超过 TREEIFY_THRESHOLD 时,链表转换为红黑树,提升性能。底层变更为数组 + 链表 + 红黑树。

37、ConcurrentHashMap 在 JDK 1.8 中,为什么要使用内置锁 synchronized 来代替重入锁 ReentrantLock?

  1. 锁粒度降低了
  2. JVM 开发团队没有放弃 synchronized,而且基于 JVM 的 synchronized 优化空间更大,更加自然。
  3. 在大量的数据操作下,对于 JVM 的内存压力,基于 API 的 ReentrantLock 会开销更多的内存。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值