Java集合面试题&知识点总结(上篇)

2023-10-29 14:25:13 浏览数 (1)

1、Java基础面试题问题
  • 问题 1. 简述 Java 集合类都有哪些?
  • 问题 2. 简述 Collection 与 Collections 的区别
  • 问题 3. 简述 List、Set、Map 三者的区别
  • 问题 4. 介绍一下 List 集合,以及它有怎样的特性?
  • 问题 5. 介绍一下 List 集合,有哪些常见的方法?
  • 问题 6. 介绍一下 ArrayList 的底层结构和相关原理
  • 问题 7. 介绍一下 ArrayList 的扩容机制
  • 问题 8. 介绍一下 ArrayList 删除元素时有哪些顾虑?
  • 问题 9. 介绍一下 ArrayList 中怎么在遍历移除一个元素?
  • 问题 10. 介绍一下 ArrayList 是线程安全的吗?如何保证 ArrayList 的线程安全?
  • 问题 11. 简述 ArrayList 与 Array 的区别
  • 问题 12. 介绍一下 LinkedList 的底层结构和相关原理
  • 问题 13. 简述 ArrayList 和 LinkedList 的区别
  • 问题 14. 介绍一下 Vector 的底层结构和相关原理
  • 问题 15. 简述 ArrayList 和 Vector 的区别
  • 问题 16. 介绍一下 Stack 的底层结构和相关原理
  • 问题 17. 简述队列和栈是什么,它们有何区别?
  • 问题 18. 请解释一下 Java 中的 Queue 和 Deque?
  • 问题 19. 请解释一下 Java 中的 PriorityQueue?
  • 问题 20. 请解释一下 Java 中的 BlockingQueue?

2、Java基础面试题解答
2.1、Java集合接口相关
  • 问题 1. 简述 Java 集合类都有哪些?

解答:Java 集合类呢主要是指 java.Util包 下的集合容器。主要包含三种:List、Set、Map,其中 List、Set 主要继承自 Collection 接口,然后它们三个又都依赖了 Iterator 迭代器;

  1. 首先,List 常用的就是 ArrayList、LinkedList、Vector,其中 ArrayList 的底层实现是数组,LinkedList 的实现是双向链表,此外 LinkedList 还实现了 Queue 队列(也在 Collection 下的接口),Vector 就是 ArrayList 的线程安全版本,但不推荐使用,此外 Java 中的栈 Stack 还继承自 Vector;
  2. 其次,Set 集合常用的就是 HashSet 和 TreeSet,它们的实现就是依赖于 HahsMap 和 TreeMap;
  3. 最后,Map 集合就是 HahsMap 和 TreeMap了。这些实现大多数都是非线程安全的。
  • 问题 2. 简述 Collection 与 Collections 的区别

解答:CollectionCollections 在 Java 中是两个不同的概念。

  1. Collection 是一个接口,它是 List、Set 和 Queue 接口的父接口,定义了适用于任何集合的操作,如 add、remove 和 contains。
  2. Collections 是一个类,它包含了一些静态的工具方法,这些方法可以对集合进行操作,如排序(sort)、查找(binarySearch)、修改(fill、copy)、同步控制(synchronizedXxx)等。
  • 问题 3. 简述 List、Set、Map 三者的区别

解答:ListSetMap 是 Java 集合框架中的三种基本接口,它们的区别主要体现在存储内容和使用方式上。

  1. List:是一个有序的集合,可以包含重复的元素。它提供了索引的访问方式,我们可以通过索引(列表的位置)来访问或者搜索列表中的元素。主要实现类有 ArrayList、LinkedList 和 Vector。
  2. Set:是一个不允许有重复元素的集合,也就是说,每个元素只能出现一次。它不提供索引访问方式,主要用于存在性检查,即检查一个元素是否存在。主要实现类有 HashSet、LinkedHashSet 和 TreeSet。
  3. Map:是一个键值对的集合,每个键映射到一个值,键不能重复,每个键最多只能映射到一个值。它提供了基于键的访问方式,我们可以通过键来获取、删除或者检查值。主要实现类有 HashMap、LinkedHashMap、TreeMap 和 Hashtable。
2.2、JavaList集合相关-特性&方法
  • 问题 4. 介绍一下 List 集合,以及它有怎样的特性?

List 是 Java 集合框架中的一个接口,它继承自 Collection 接口。List 集合中的元素是有序的,并且可以包含重复的元素。

List 集合的主要特性包括:

  1. 有序:List 集合中的元素按照它们被插入的顺序进行存储。也就是说,我们可以通过索引来访问 List 集合中的任何位置的元素。
  2. 可重复:List 集合允许插入重复的元素。也就是说,同一个对象可以出现在 List 集合的任何位置。
  3. 支持索引:List 集合支持随机访问,我们可以直接通过索引(列表的位置)来获取、插入、删除元素。
  4. 元素可为 null:List 集合中可以添加 null 元素。

Java 中的 ArrayListLinkedListVector 都是 List 接口的实现类,它们具有上述的 List 特性,但是在内部实现和性能上有所不同。例如,ArrayList 是基于动态数组实现的,适合随机访问;LinkedList 是基于双向链表实现的,适合插入和删除操作;Vector 是线程安全的,适合在多线程环境下使用。

  • 问题 5. 介绍一下 List 集合,有哪些常见的方法?

List 接口在 Collection 接口的基础上,增加了一些针对元素位置(索引)操作的方法。以下是 List 接口中一些常见的方法:

  1. void add(int index, E element):在指定位置插入元素。
  2. boolean add(E e):在列表末尾添加元素。
  3. E get(int index):返回指定位置的元素。
  4. int indexOf(Object o):返回指定元素首次出现的位置,如果列表不包含该元素,则返回 -1。
  5. int lastIndexOf(Object o):返回指定元素最后一次出现的位置,如果列表不包含该元素,则返回 -1。
  6. ListIterator<E> listIterator():返回列表中元素的列表迭代器。
  7. E remove(int index):移除指定位置的元素。
  8. E set(int index, E element):用指定元素替换指定位置的元素。
  9. List<E> subList(int fromIndex, int toIndex):返回列表中指定的 fromIndex(包含)和 toIndex(不包含)之间的部分视图。

以上就是 List 接口中一些常见的方法,它们提供了丰富的功能,使得我们可以方便地对列表进行操作。

2.3、JavaList集合相关-ArrayList
  • 问题 6. 介绍一下 ArrayList 的底层结构和相关原理

解答:ArrayList 是 Java 中常用的一种动态数组实现,其底层是基于数组实现的。

  1. 存储结构:ArrayList 内部使用一个数组(elementData)来存储元素。当添加元素时,如果数组已满,就会创建一个新的更大的数组,并将原数组的内容复制到新数组中,这个过程称为扩容。
  2. 扩容机制:ArrayList 的扩容机制是,每次扩容时,新数组的大小是原数组大小的 1.5 倍。如果这个值仍然不足以满足需求,那么新数组的大小就直接设置为需求的大小。
  3. 插入和删除:ArrayList 在尾部插入和删除元素非常高效,时间复杂度为 O(1)。但是在中间或头部插入和删除元素需要移动大量元素,时间复杂度为 O(n)。
  4. 访问元素:由于底层是数组,所以 ArrayList 支持随机访问,按索引访问元素的时间复杂度为 O(1)。
  5. 线程安全:ArrayList 是非线程安全的,如果需要在多线程环境下使用,可以使用 Collections.synchronizedList() 方法返回一个线程安全的 ArrayList,或者使用线程安全的 Vector

总的来说,ArrayList 是一种动态数组结构,适合随机访问场景,但在中间或头部插入和删除元素时效率较低。

  • 问题 7. 介绍一下 ArrayList 的扩容机制

解答:ArrayList 的扩容机制是这样的:

  1. 当我们向 ArrayList 添加元素时,如果当前数组已满(即数组的大小等于其元素的数量),那么 ArrayList 就需要进行扩容。
  2. ArrayList 的扩容过程是创建一个新的数组,这个新数组的大小是原数组大小的 1.5 倍(即原数组大小加上原数组大小的一半)。具体来说,如果原数组大小为 10,那么新数组的大小就是 15。
  3. 创建新数组后,ArrayList 会将原数组中的所有元素复制到新数组中,然后丢弃原数组。
  4. 这个扩容过程是自动进行的,我们在使用 ArrayList 时无需关心其扩容机制。

需要注意的是,ArrayList 的这种扩容机制意味着其在添加大量元素时可能会有一定的性能开销,因为每次扩容都需要创建新数组并复制元素。如果我们预先知道 ArrayList 将要存储的元素数量,可以在创建 ArrayList 时指定其初始大小,这样可以减少扩容操作,提高性能。

  • 问题 8. 介绍一下 ArrayList 删除元素时有哪些顾虑?

解答:在使用 ArrayList 删除元素时,需要注意以下几点:

  1. ConcurrentModificationException:在遍历 ArrayList 的过程中直接调用 ArrayListremove() 方法删除元素,可能会抛出 ConcurrentModificationException 异常。正确的做法是使用 Iteratorremove() 方法删除元素。
  2. 性能考虑:ArrayList 在删除元素时,为了保持元素的连续性,会将后面的元素向前移动,所以删除元素的性能与 ArrayList 的大小成正比。如果需要频繁删除元素,可以考虑使用 LinkedList
  3. 索引越界:在调用 remove(int index) 方法时,如果索引超出了 ArrayList 的范围,会抛出 IndexOutOfBoundsException 异常。所以在删除元素前,需要确保索引是有效的。
  4. 自动装箱:ArrayListremove() 方法有两个重载版本:remove(int index)remove(Object o)。如果 ArrayList 存储的是基本类型的包装类,比如 Integer,那么在调用 remove() 方法时需要注意自动装箱可能带来的问题。例如,list.remove(1) 可能不是删除元素 1,而是删除索引为 1 的元素。
  5. 空指针:如果 ArrayList 中存储的是对象,那么在删除元素时,如果 ArrayList 中存在 null,需要注意 NullPointerException 异常。
  • 问题 9. 介绍一下 ArrayList 中怎么在遍历移除一个元素?

解答:在遍历 ArrayList 时移除元素,需要注意 ConcurrentModificationException 异常。以下是几种常见的移除元素的方法:

  1. 使用 Iteratorremove() 方法。这是最安全且推荐的方式。
代码语言:javascript复制
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    if (需要移除的条件) {
        iterator.remove();
    }
}
  1. 使用 Listremove() 方法,但需要倒序遍历。
代码语言:javascript复制
for (int i = list.size() - 1; i >= 0; i--) {
    if (需要移除的条件) {
        list.remove(i);
    }
}
  1. 使用 Java 8 的 Collection.removeIf() 方法。
代码语言:javascript复制
list.removeIf(item -> 需要移除的条件);

以上三种方法都可以在遍历 ArrayList 时移除元素,但是推荐使用 Iteratorremove() 方法,因为它在面对并发修改时可以提供正确的行为。

  • 问题 10. 介绍一下 ArrayList 是线程安全的吗?如何保证 ArrayList 的线程安全?

解答:ArrayList 是非线程安全的,它的方法没有进行同步处理,所以在多线程环境下可能会出现问题。

如果需要在多线程环境下使用 ArrayList,有以下几种方式可以保证线程安全:

  1. 使用 Collections.synchronizedList() 方法创建一个同步的 ArrayList
代码语言:javascript复制
List<String> list = Collections.synchronizedList(new ArrayList<>());

这个方法会返回一个线程安全的 List,所有的方法都进行了同步处理。

  1. 使用 CopyOnWriteArrayList,这是一个线程安全的 List 实现:
代码语言:javascript复制
List<String> list = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList 在每次修改操作(如添加、删除元素)时,都会复制一份新的数组,这样可以避免修改操作对遍历操作的影响,提供了一种读写分离的机制。

  1. 使用并发包 java.util.concurrent 中的其他线程安全集合,如 ConcurrentLinkedQueueConcurrentHashMap 等。
  2. 使用 synchronized 关键字或 Lock 对象手动同步 ArrayList 的操作。

需要注意的是,以上方法在提供线程安全的同时,可能会带来一定的性能开销。

  • 问题 11. 简述 ArrayList 与 Array 的区别

解答:ArrayArrayList 是 Java 中两种不同的数据结构,它们的主要区别在于大小的可变性、性能、类型限制和功能。

  1. 大小可变性:Array 是固定长度的,一旦创建,其大小就不能改变。ArrayList 是动态的,可以自动调整其大小以适应元素的添加和删除。
  2. 性能:Array 在访问元素时具有更好的性能,因为它是基于索引的数据结构。ArrayList 在添加和删除元素时具有更好的性能,特别是在列表的末尾,因为它可以动态调整大小。
  3. 类型限制:Array 可以存储基本数据类型或对象。ArrayList 只能存储对象,不能直接存储基本数据类型。
  4. 功能:Array 是一个简单的数据结构,没有提供很多功能。ArrayList 是一个集合类,提供了大量的方法,如添加、删除、遍历等。

总的来说,ArrayArrayList 各有优势,选择哪种取决于具体的需求。

2.3、JavaList集合相关-LinkedList
  • 问题 12. 介绍一下 LinkedList 的底层结构和相关原理

解答:LinkedList 是 Java 中常用的一种链表实现,其底层是基于双向链表实现的。

  1. 存储结构:LinkedList 内部使用一个双向链表来存储元素。每个元素(节点)都包含了对前一个元素和后一个元素的引用。
  2. 插入和删除:LinkedList 在链表头部和尾部插入和删除元素非常高效,时间复杂度为 O(1)。在链表中间插入和删除元素需要先找到对应的位置,时间复杂度为 O(n)。
  3. 访问元素:LinkedList 不支持高效的随机访问,访问特定索引的元素需要从头(或尾)开始遍历,时间复杂度为 O(n)。
  4. 线程安全:LinkedList 是非线程安全的,如果需要在多线程环境下使用,可以使用 Collections.synchronizedList() 方法返回一个线程安全的 LinkedList

总的来说,LinkedList 是一种链表结构,适合插入和删除操作频繁的场景,但在访问元素时效率较低。

  • 问题 13. 简述 ArrayList 和 LinkedList 的区别

解答:ArrayListLinkedList 都是 List 接口的实现,但它们在内部数据结构和性能上有一些区别:

  1. 内部数据结构:ArrayList 是基于动态数组实现的,支持随机访问,按索引访问元素非常快,时间复杂度为 O(1)。LinkedList 是基于双向链表实现的,不支持高效的随机访问,按索引访问元素需要从头(或尾)开始遍历,时间复杂度为 O(n)。
  2. 插入和删除:ArrayList 的插入和删除操作需要进行数组元素的移动(除非插入和删除操作在列表末尾进行),所以插入和删除元素的时间复杂度为 O(n)。LinkedList 的插入和删除操作只需要改变节点的引用,所以在列表中间插入和删除元素的时间复杂度为 O(1)(前提是已经获取到了要插入位置的节点)。
  3. 内存占用:ArrayList 的内存占用相对较低,因为它只需要存储元素数据和数组的引用。LinkedList 的内存占用较高,因为它需要额外存储节点之间的引用。

总的来说,ArrayList 更适合随机访问场景,LinkedList 更适合插入和删除操作频繁的场景。

2.4、JavaList集合相关-Vector
  • 问题 14. 介绍一下 Vector 的底层结构和相关原理

解答:Vector 是 Java 中的一种线程安全的动态数组实现,其底层是基于数组实现的。

  1. 存储结构:Vector 内部使用一个数组(elementData)来存储元素。当添加元素时,如果数组已满,就会创建一个新的更大的数组,并将原数组的内容复制到新数组中,这个过程称为扩容。
  2. 扩容机制:Vector 的扩容机制是,每次扩容时,新数组的大小是原数组大小的 2 倍。如果这个值仍然不足以满足需求,那么新数组的大小就直接设置为需求的大小。
  3. 插入和删除:Vector 在尾部插入和删除元素非常高效,时间复杂度为 O(1)。但是在中间或头部插入和删除元素需要移动大量元素,时间复杂度为 O(n)。
  4. 访问元素:由于底层是数组,所以 Vector 支持随机访问,按索引访问元素的时间复杂度为 O(1)。
  5. 线程安全:Vector 的所有公共方法都进行了同步处理,所以它是线程安全的。但这也意味着在单线程环境下,Vector 的性能会比 ArrayList 差一些。

总的来说,Vector 是一种线程安全的动态数组结构,适合在多线程环境下使用,但在单线程环境下,由于同步处理带来的开销,其性能会比 ArrayList 差一些。

  • 问题 15. 简述 ArrayList 和 Vector 的区别

解答:ArrayListVector 都是实现了 List 接口的类,它们都是基于动态数组实现的,但是在同步性和性能上有一些区别:

  1. 同步性:Vector 是线程安全的,它的大部分方法都进行了同步处理,可以在多线程环境下使用。实现上其实就是 Vector 在 ArrayList 的方法前面加上了 Synchronized。ArrayList 是非线程安全的,它的方法没有进行同步处理,所以在多线程环境下可能会出现问题。
  2. 性能:由于 Vector 进行了同步处理,所以在单线程环境下,Vector 的性能会比 ArrayList 差一些。ArrayList 在单线程环境下的性能比 Vector 好,因为它没有进行同步处理。
  3. 扩容:ArrayList 在每次需要扩容时,都会增加到原来的 1.5 倍。Vector 在每次需要扩容时,都会增加到原来的 2 倍。

总的来说,如果需要在多线程环境下使用,可以选择 Vector,如果是在单线程环境下,或者已经通过其他方式处理了同步问题,那么 ArrayList 会是更好的选择。

2.4、JavaList集合相关-Stack
  • 问题 16. 介绍一下 Stack 的底层结构和相关原理

解答:Stack 是 Java 中的一种后进先出(LIFO)的数据结构,其底层是基于 Vector 实现的。

  1. 存储结构:Stack 内部使用一个 Vector 来存储元素。当添加元素(压栈)时,元素被添加到 Vector 的末尾;当删除元素(弹栈)时,元素从 Vector 的末尾被移除。
  2. 扩容机制:由于 Stack 是基于 Vector 实现的,所以其扩容机制与 Vector 相同。每次扩容时,新数组的大小是原数组大小的 2 倍。
  3. 插入和删除:Stack 的插入和删除操作都在 Vector 的末尾进行,所以非常高效,时间复杂度为 O(1)。
  4. 访问元素:Stack 提供了 peek() 方法来查看栈顶元素,时间复杂度为 O(1)。由于底层是 Vector,所以 Stack 也支持按索引访问元素,但这并不常用。
  5. 线程安全:由于 Stack 是基于 Vector 实现的,所以它也是线程安全的。但这也意味着在单线程环境下,Stack 的性能会比基于 ArrayList 实现的 Deque 差一些。

总的来说,Stack 是一种后进先出的数据结构,适合在需要后进先出操作的场景下使用,如函数调用栈、回溯算法等。

2.5、JavaList集合相关-Queue
  • 问题 17. 简述队列和栈是什么,它们有何区别?

解答:队列(Queue)和栈(Stack)是两种常见的数据结构,它们在数据的存储和访问方式上有一些区别。

  1. 队列(Queue):队列是一种先进先出(FIFO,First In First Out)的数据结构,新元素添加到队列的尾部,而移除元素则从队列的头部开始。队列常用于实现需要按照元素添加顺序进行处理的场景,如任务队列、消息队列等。
  2. 栈(Stack):栈是一种后进先出(LIFO,Last In First Out)的数据结构,新元素添加到栈的顶部,移除元素也从栈的顶部开始。栈常用于实现需要后进先出操作的场景,如函数调用栈、撤销操作、深度优先搜索等。

总的来说,队列和栈的主要区别在于元素的访问顺序:队列是先进先出,而栈是后进先出。

  • 问题 18. 请解释一下 Java 中的 Queue 和 Deque?

QueueDeque 是 Java 中的两种接口,分别代表队列和双端队列这两种数据结构。

  1. Queue:队列是一种先进先出(FIFO)的数据结构,支持在队尾插入元素(offer 方法),在队头删除元素(poll 方法),查看队头元素(peek 方法)。常用的 Queue 实现类有 LinkedListPriorityQueue 等。
  2. Deque:双端队列是一种特殊的队列,它支持在两端插入和删除元素。除了支持 Queue 的所有操作外,还支持在队头插入元素(offerFirst 方法),在队尾删除元素(pollLast 方法),查看队尾元素(peekLast 方法)。常用的 Deque 实现类有 ArrayDequeLinkedList 等。

在实际使用时,可以根据具体需求选择使用 Queue 还是 Deque,以及选择合适的实现类。例如,如果需要按元素的优先级进行处理,可以使用 PriorityQueue;如果需要在两端插入和删除元素,可以使用 Deque

  • 问题 19. 请解释一下 Java 中的 PriorityQueue?

解答:PriorityQueue 是 Java 中的一种特殊的队列,它的特点是队列中的元素按照它们的优先级进行排序。

  1. 存储结构:PriorityQueue 内部使用一个二叉小顶堆来实现。二叉小顶堆是一种特殊的二叉树,树中任意一个非叶子节点的值都不大于其子节点的值,树的根节点(顶部)是所有节点中的最小值。
  2. 元素排序:PriorityQueue 中的元素可以自然排序,或者通过提供的 Comparator 进行自定义排序。在添加元素时,会根据元素的优先级找到合适的位置保证堆的性质。
  3. 插入和删除:插入元素和删除元素(或者说是获取并删除最高优先级的元素)的时间复杂度都是 O(log n)。
  4. 访问元素:PriorityQueue 提供了 peek 方法来访问最高优先级的元素(堆顶元素),时间复杂度为 O(1)。
  5. 线程安全:PriorityQueue 是非线程安全的,如果需要在多线程环境下使用,可以使用 PriorityBlockingQueue

总的来说,PriorityQueue 是一种可以动态调整内部元素顺序的队列,适合实现需要动态排序的场景,如任务调度、Dijkstra 算法、Huffman 编码等。

  • 问题 20. 请解释一下 Java 中的 BlockingQueue?

解答:BlockingQueue 是 Java 中的一个接口,它是一种特殊的队列,主要提供了阻塞操作的支持,适用于生产者消费者模式。

  1. 阻塞操作:BlockingQueue 提供了 puttake 方法,当队列满时,put 方法会阻塞直到队列不满;当队列空时,take 方法会阻塞直到队列不空。这样可以简化生产者消费者模式的实现,无需显式地进行同步和唤醒操作。
  2. 非阻塞操作:BlockingQueue 也提供了非阻塞操作 offerpoll,如果无法立即执行操作,这些方法会返回一个特殊值(如 nullfalse)而不是阻塞。
  3. 超时操作:BlockingQueue 还提供了带超时的 offerpoll 方法,如果在指定的时间内无法执行操作,这些方法会返回一个特殊值。
  4. 实现类:BlockingQueue 的常用实现类有 ArrayBlockingQueueLinkedBlockingQueuePriorityBlockingQueueSynchronousQueue 等,它们分别适用于不同的场景。

总的来说,BlockingQueue 是一种支持阻塞操作的队列,适合实现生产者消费者模式,可以简化多线程编程。

0 人点赞