JAVA:集合框架常见的面试题和答案

1、List接口的常见实现类有哪些?

答: 常见的List接口实现类包括:

  • ArrayList: 基于动态数组实现的List,支持快速随机访问。
  • LinkedList: 基于链表实现的List,支持快速的插入和删除操作。
  • Vector: 一个线程安全的动态数组,通常不建议使用,可以用ArrayList代替。
  • Stack: 继承自Vector,表示一个后进先出(LIFO)的堆栈。

2、ArrayList和LinkedList的区别是什么?

  • 数据结构实现:ArrayList :基于数组,便于按 index 访问,超过数组需要扩容,扩容成本较高。LinkedList:使用链表实现,无需扩容。
  • 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。
  • 增加和删除效率:在非首尾的增删操作,LinkedList 要比ArrayList 效率要高,因为ArrayList 增删操作要影响数组内的其他数据的下标。
  • 内存空间占用:LinkedList 比 ArrayList 更占内存,因为LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
  • 线程安全:ArrayList 和 LinkList 都是不同步的,不保证线程安全。
  • 综合来说,需要频繁读取集合中的元素时,更推荐使用 Arrayist,而在增删操作较多时,更推荐使用 LinkedList。
  • LinkedList 的双向链表是链表的一种,它的每个数据结点中都有2 个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便的访问它的前驱结点和后继结点。

3、Map接口的常见实现类有哪些?

答: 常见的Map接口实现类包括:

  • HashMap: 基于哈希表实现的Map,提供了快速的查找性能。
  • TreeMap: 基于红黑树实现的有序Map,按键的自然顺序或者定制的顺序进行排序。
  • LinkedHashMap: 基于哈希表和双向链表实现的Map,可以按照插入顺序或者访问顺序进行遍历。
  • Hashtable: 一个线程安全的哈希表,通常不建议使用,可以用HashMap代替。
  • ConcurrentHashMap: 一个线程安全的哈希表,支持并发操作。

4、HashTable,HashMap,TreeMap区别?

  • HashTable线程同步,HashMap,TreeMap非线程同步。
  • HashTable,TreeMap不允许<键,值>有空值,HashMap允许<键,值>有空值。
  • HashTable中hash数组的默认大小是11,增加方式的old*2+1,HashMap中hash数组的默认大小是16,增长方式一定是2的指数倍。
  • TreeMap能够把它保存的记录根据键排序,默认是按升序排序,因此如果你的键需要有序,建议使用TreeMap代替HashMap。

5、加载因子(扩容因子)为何默认是0.75?

  • 在空间占用与查询时间之间取得了较好的权衡
  • 大于这个值,空间节省了,但是链表可能过长就会影响性能
  • 小于这个值,冲突减少了,但是扩容就会比较频繁,空间占用多并且扩容也会消耗性能

6、LinkedHashMap的应用,底层,原理

答:LinkedHashMap维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序(insert-order)或者是访问顺序,其中默认的迭代访问顺序就是插入顺序,即可以按插入的顺序遍历元素,这点和HashMap有很大的不同。
LRU算法可以用LinkedHashMap实现。

7、HashMap 的底层数据结构是怎样的 ?

JDK1.8 之前

  • JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是链表散列。
  • HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到hash 值,然后通过(n-1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
  • 所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。

JDK1.8 之后

  • 当链表长度大于阈值(默认为 8)时,会首先调用 treeifyBin()方法。这个方法会根据HashMap 数组来决定是否转换为红黑树。只有当数组长度大于或者等于64的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是执行resize() 方法对数组扩容。

8、HashMap 的扩容机制是怎样的?

一般情况下,当元素数量超过阈值时便会触发扩容。每次扩容的容量都是之前容量的2倍。HashMap 的容量是有上限的,必须小于 1<<30,即 1073741824。如果容量超出了这个数,则不再增长,且阈值会被设置为 Integer.MAX_VALUE。

JDK7 中的扩容机制

  • 空参数的构造函数:以默认容量、默认负载因子、默认阈值初始化数组。内部数组是空数组。
  • 有参构造函数:根据参数确定容量、负载因子、阈值等。
  • 第一次 put 时会初始化数组,其容量变为不小于指定容量的2 的幂数,然后根据负载因子确定阈值。
  • 如果不是第一次扩容,则 新容量=旧容量 x 2 ,新阈值=新容量x 负载因子。JDK8 的扩容机制
  • 空参数的构造函数:实例化的 HashMap 默认内部数组是null,即没有实例化。第一次调用 put 方法时,则会开始第一次初始化扩容,长度为 16。
  • 有参构造函数:用于指定容量。会根据指定的正整数找到不小于指定容量的2的幂数,将这个数设置赋值给阈值(threshold)。第一次调用 put 方法时,会将阈值赋值给容量,然后让 阈值 = 容量 x 负载因子。
  • 如果不是第一次扩容,则容量变为原来的 2 倍,阈值也变为原来的2 倍。(容量和阈值都变为原来的 2 倍时,负载因子还是不变)。
    此外还有几个细节需要注意:
  • 首次 put 时,先会触发扩容(算是初始化),然后存入数据,然后判断是否需要扩容;
  • 不是首次 put,则不再初始化,直接存入数据,然后判断是否需要扩容;

9、ConcurrentHashMap 的存储结构是怎样的?

  • Java7 中 ConcurrnetHashMap 使用的分段锁,也就是每一个Segment 上同时只有一个线程可以操作,每一个 Segment 都是一个类似 HashMap 数组的结构,它可以扩容,它的冲突会转化为链表。但是 Segment 的个数一但初始化就不能改变,默认Segment的个数是 16 个。
  • Java8 中的 ConcurrnetHashMap 使用的 Synchronized 锁加CAS 的机制。结构也由Java7 中的 Segment 数组 + HashEntry 数组 + 链表 进化成了Node 数组+链表/红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。

10、用过 ConcurrentHashMap,讲一下他和HashTable 的不同之处?

  • HashTable 就是实现了 HashMap 加上了 synchronized,而ConcurrentHashMap底层采用分段的数组+链表实现,线程安全
  • ConcurrentHashMap 通过把整个 Map 分为 N 个 Segment,可以提供相同的线程安全,但是效率提升 N 倍,默认提升 16 倍。
  • 并且读操作不加锁,由于 HashEntry 的 value 变量是 volatile 的,也能保证读取到最新的值。
  • Hashtable 的 synchronized 是针对整张 Hash 表的,即每次锁住整张表让线程独占,ConcurrentHashMap 允许多个修改操作并发进行,其关键在于使用了锁分离技术
  • 扩容:段内扩容(段内元素超过该段对应 Entry 数组长度的75%触发扩容,不会对整个Map 进行扩容),插入前检测需不需要扩容,有效避免无效扩容

11、比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同

  • HashSet 是 Set 接口的主要实现类 ,HashSet 的底层是HashMap,线程不安全的,可以存储 null 值;
  • LinkedHashSet 是 HashSet 的子类,能够按照添加的顺序遍历;
  • TreeSet 底层使用红黑树,元素是有序的,排序的方式有自然排序和定制排序。

12、Comparable 和 Comparator 的区别?

  • comparable 接口实际上是出自 java.lang 包 它有一个 compareTo(Object obj)方法用来排序
  • comparator 接口实际上是出自 java.util 包它有一个 compare(Object obj1, Objectobj2)方法用来排序
    一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo()方法或compare()方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写 compareTo()方法和使用自制的Comparator 方法或者以两个 Comparator 来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 Collections.sort()。

13、什么是哈希冲突?

答:当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。

14、CopyOnWriteArrayList与ArrayList、Vector的区别

答:CopyOnWriteArrayList与ArrayList、Vector有以下主要区别:

  • 线程安全性:CopyOnWriteArrayList是线程安全的,而ArrayList不是;Vector也是线程安全的,但它使用全局锁,导致性能较差。
  • 读写性能:CopyOnWriteArrayList具有较高的并发读性能,但写操作性能较差,因为每次写操作都需要复制一个新的副本。ArrayList具有较高的读写性能,但在多线程环境下可能出现线程安全问题。Vector的读写性能较差,因为它使用全局锁。
  • 内存占用:CopyOnWriteArrayList在写操作时需要复制一个新的副本,因此可能导致较高的内存占用。ArrayList和Vector的内存占用相对较低。
  • 实时性:CopyOnWriteArrayList的迭代器只能获取到写操作前的数据副本,因此在迭代过程中无法获取实时数据。ArrayList和Vector的迭代器可以获取实时数据,但在多线程环境下可能会导致线程安全问题。

15、如何将Map转换为List?

答: 可以使用ArrayList的构造函数或者addAll方法将Map转换为List。

使用构造函数:

List> list = new ArrayList<>(map.entrySet());

使用addAll方法:

List> list = new ArrayList<>();
list.addAll(map.entrySet());

16、LinkedHashSet和CopyOnWriteArraySet的区别

答:LinkedHashSet和CopyOnWriteArraySet是Java集合框架中不同的实现类,它们具有不同的特性和适用场景。

LinkedHashSet:

  • 内部实现:
    LinkedHashSet 是基于哈希表和双向链表实现的,它继承自HashSet,保留了元素插入的顺序。
  • 有序性:
    LinkedHashSet 保持了元素插入的顺序,因此可以按照插入顺序进行遍历。
  • 性能:
    由于它的底层实现依赖于哈希表,因此查找元素的速度非常快。
  • 线程安全:
    LinkedHashSet 是非线程安全的。
  • 适用场景:
    当你需要保留元素插入的顺序,并且不需要考虑线程安全时,可以使用LinkedHashSet。

CopyOnWriteArraySet:

  • 内部实现:
    CopyOnWriteArraySet 是基于CopyOnWriteArrayList实现的,它是一个线程安全的Set。
  • 线程安全:
    CopyOnWriteArraySet 是线程安全的,它的所有修改操作都是在一个新的副本上进行的,而不会影响原始集合。
  • 写时复制:
    当需要对集合进行修改时,CopyOnWriteArraySet 会先复制一个新的数组,然后在新的数组上进行修改,最后将新的数组替换原始数组。
  • 适用场景:
    当你需要在多线程环境下保证安全地对集合进行修改时,可以使用CopyOnWriteArraySet。它适用于读多写少的场景。

17、Java中Queue接口的常见实现类有哪些?

答: 常见的Queue接口实现类包括LinkedList、PriorityQueue和ArrayDeque。

18、LinkedList和ArrayDeque之间有什么区别?

  • LinkedList 是一个双向链表实现的队列,可以作为队列和双端队列使用。
  • ArrayDeque 是一个基于动态数组实现的双端队列,它的性能比LinkedList稍好,但不支持索引访问。

19、PriorityQueue是什么?它是如何工作的?

答: PriorityQueue 是一个优先级队列,它根据元素的优先级来确定出队顺序。默认情况下,PriorityQueue 是一个最小堆实现,可以通过提供一个自定义的Comparator来实现最大堆。

20、队列和栈有什么区别?

  • 队列是一种先进先出(FIFO)的数据结构,只允许在队列的一端插入元素,在另一端删除元素。
  • 栈是一种后进先出(LIFO)的数据结构,只允许在栈的一端插入和删除元素。

21、如何在多线程环境中使用BlockingQueue?

答案: 可以通过创建一个BlockingQueue实例,然后在生产者线程中使用put方法向队列中添加元素,在消费者线程中使用take方法获取元素。

BlockingQueue queue = new LinkedBlockingQueue<>();
// 生产者线程
queue.put(element);
// 消费者线程
ElementType element = queue.take();

22、ConcurrentLinkedQueue和LinkedBlockingQueue有什么区别?

  • ConcurrentLinkedQueue 是一个基于非阻塞算法实现的非阻塞队列,它在高并发环境下表现良好。
  • LinkedBlockingQueue 是一个基于锁实现的阻塞队列,它可以在队列为空或者满时阻塞线程。

23、如何实现一个自定义的优先级队列?

答案: 可以使用PriorityQueue并提供一个自定义的比较器(Comparator)来实现一个自定义的优先级队列。

PriorityQueue priorityQueue = new PriorityQueue<>(new MyComparator()

24、ArrayBlockingQueue和LinkedBlockingQueue的区别

答:ArrayBlockingQueue和LinkedBlockingQueue是Java中两种不同类型的阻塞队列实现,它们有一些重要的区别。

ArrayBlockingQueue:

  • 内部实现:
    ArrayBlockingQueue 是一个基于数组实现的有界阻塞队列,它按照先进先出(FIFO)的原则对元素进行排序。
  • 容量限制:
    ArrayBlockingQueue 在创建时需要指定一个固定的容量,即队列中可以存放的最大元素数量。
  • 阻塞特性:
    当队列已满时,ArrayBlockingQueue 会阻塞等待直到有空间可以放置新元素。
    当队列为空时,尝试从中获取元素的操作会被阻塞等待,直到队列中有新的元素。
  • 性能:
    由于底层是基于数组实现的,因此在性能上比LinkedBlockingQueue更高效,但它的容量是固定的。

LinkedBlockingQueue:

  • 内部实现:
    LinkedBlockingQueue 是一个基于链表实现的无界阻塞队列,它按照先进先出(FIFO)的原则对元素进行排序。
  • 容量限制:
    LinkedBlockingQueue 的容量理论上可以是无限大,因此它不会因为达到最大容量而阻塞。
  • 阻塞特性:
    当队列为空时,尝试从中获取元素的操作会被阻塞等待,直到队列中有新的元素。
    当队列已满时,LinkedBlockingQueue 会阻塞等待直到有空间可以放置新元素。
  • 性能:
    由于底层是基于链表实现的,相对于ArrayBlockingQueue来说,它在高并发场景下可能会略显低效。
    JAVA:集合框架常见的面试题和答案_第1张图片

你可能感兴趣的:(面试题,java,开发语言)