大家好,我是栗筝i,从 2022 年 10 月份开始,我持续梳理出了全面的 Java 技术栈内容,一方面是对自己学习内容进行整合梳理,另一方面是希望对大家有所帮助,使我们一同进步。得到了很多读者的正面反馈。
而在 2023 年 10 月份开始,我将推出 Java 面试题/知识点系列内容,期望对大家有所助益,让我们一起提升。
本篇是对 Java 集合系列的面试题 / 知识点的总结的上篇
解答:Java 集合类呢主要是指 java.Util包 下的集合容器。主要包含三种:List、Set、Map,其中 List、Set 主要继承自 Collection 接口,然后它们三个又都依赖了 Iterator 迭代器;
解答:Collection
和 Collections
在 Java 中是两个不同的概念。
Collection
是一个接口,它是 List、Set 和 Queue 接口的父接口,定义了适用于任何集合的操作,如 add、remove 和 contains。
Collections
是一个类,它包含了一些静态的工具方法,这些方法可以对集合进行操作,如排序(sort)、查找(binarySearch)、修改(fill、copy)、同步控制(synchronizedXxx)等。
解答:List
、Set
和 Map
是 Java 集合框架中的三种基本接口,它们的区别主要体现在存储内容和使用方式上。
List
:是一个有序的集合,可以包含重复的元素。它提供了索引的访问方式,我们可以通过索引(列表的位置)来访问或者搜索列表中的元素。主要实现类有 ArrayList、LinkedList 和 Vector。
Set
:是一个不允许有重复元素的集合,也就是说,每个元素只能出现一次。它不提供索引访问方式,主要用于存在性检查,即检查一个元素是否存在。主要实现类有 HashSet、LinkedHashSet 和 TreeSet。
Map
:是一个键值对的集合,每个键映射到一个值,键不能重复,每个键最多只能映射到一个值。它提供了基于键的访问方式,我们可以通过键来获取、删除或者检查值。主要实现类有 HashMap、LinkedHashMap、TreeMap 和 Hashtable。
List
是 Java 集合框架中的一个接口,它继承自 Collection
接口。List
集合中的元素是有序的,并且可以包含重复的元素。
List
集合的主要特性包括:
有序:List
集合中的元素按照它们被插入的顺序进行存储。也就是说,我们可以通过索引来访问 List
集合中的任何位置的元素。
可重复:List
集合允许插入重复的元素。也就是说,同一个对象可以出现在 List
集合的任何位置。
支持索引:List
集合支持随机访问,我们可以直接通过索引(列表的位置)来获取、插入、删除元素。
元素可为 null:List
集合中可以添加 null 元素。
Java 中的 ArrayList
、LinkedList
和 Vector
都是 List
接口的实现类,它们具有上述的 List
特性,但是在内部实现和性能上有所不同。例如,ArrayList
是基于动态数组实现的,适合随机访问;LinkedList
是基于双向链表实现的,适合插入和删除操作;Vector
是线程安全的,适合在多线程环境下使用。
List
接口在 Collection
接口的基础上,增加了一些针对元素位置(索引)操作的方法。以下是 List
接口中一些常见的方法:
void add(int index, E element)
:在指定位置插入元素。
boolean add(E e)
:在列表末尾添加元素。
E get(int index)
:返回指定位置的元素。
int indexOf(Object o)
:返回指定元素首次出现的位置,如果列表不包含该元素,则返回 -1。
int lastIndexOf(Object o)
:返回指定元素最后一次出现的位置,如果列表不包含该元素,则返回 -1。
ListIterator
:返回列表中元素的列表迭代器。
E remove(int index)
:移除指定位置的元素。
E set(int index, E element)
:用指定元素替换指定位置的元素。
List
:返回列表中指定的 fromIndex(包含)和 toIndex(不包含)之间的部分视图。
以上就是 List
接口中一些常见的方法,它们提供了丰富的功能,使得我们可以方便地对列表进行操作。
解答:ArrayList
是 Java 中常用的一种动态数组实现,其底层是基于数组实现的。
存储结构:ArrayList
内部使用一个数组(elementData
)来存储元素。当添加元素时,如果数组已满,就会创建一个新的更大的数组,并将原数组的内容复制到新数组中,这个过程称为扩容。
扩容机制:ArrayList
的扩容机制是,每次扩容时,新数组的大小是原数组大小的 1.5 倍。如果这个值仍然不足以满足需求,那么新数组的大小就直接设置为需求的大小。
插入和删除:ArrayList
在尾部插入和删除元素非常高效,时间复杂度为 O(1)。但是在中间或头部插入和删除元素需要移动大量元素,时间复杂度为 O(n)。
访问元素:由于底层是数组,所以 ArrayList
支持随机访问,按索引访问元素的时间复杂度为 O(1)。
线程安全:ArrayList
是非线程安全的,如果需要在多线程环境下使用,可以使用 Collections.synchronizedList()
方法返回一个线程安全的 ArrayList
,或者使用线程安全的 Vector
。
总的来说,ArrayList
是一种动态数组结构,适合随机访问场景,但在中间或头部插入和删除元素时效率较低。
解答:ArrayList
的扩容机制是这样的:
当我们向 ArrayList
添加元素时,如果当前数组已满(即数组的大小等于其元素的数量),那么 ArrayList
就需要进行扩容。
ArrayList
的扩容过程是创建一个新的数组,这个新数组的大小是原数组大小的 1.5 倍(即原数组大小加上原数组大小的一半)。具体来说,如果原数组大小为 10,那么新数组的大小就是 15。
创建新数组后,ArrayList
会将原数组中的所有元素复制到新数组中,然后丢弃原数组。
这个扩容过程是自动进行的,我们在使用 ArrayList
时无需关心其扩容机制。
需要注意的是,ArrayList
的这种扩容机制意味着其在添加大量元素时可能会有一定的性能开销,因为每次扩容都需要创建新数组并复制元素。如果我们预先知道 ArrayList
将要存储的元素数量,可以在创建 ArrayList
时指定其初始大小,这样可以减少扩容操作,提高性能。
解答:在使用 ArrayList
删除元素时,需要注意以下几点:
ConcurrentModificationException
:在遍历 ArrayList
的过程中直接调用 ArrayList
的 remove()
方法删除元素,可能会抛出 ConcurrentModificationException
异常。正确的做法是使用 Iterator
的 remove()
方法删除元素。
性能考虑:ArrayList
在删除元素时,为了保持元素的连续性,会将后面的元素向前移动,所以删除元素的性能与 ArrayList
的大小成正比。如果需要频繁删除元素,可以考虑使用 LinkedList
。
索引越界:在调用 remove(int index)
方法时,如果索引超出了 ArrayList
的范围,会抛出 IndexOutOfBoundsException
异常。所以在删除元素前,需要确保索引是有效的。
自动装箱:ArrayList
的 remove()
方法有两个重载版本:remove(int index)
和 remove(Object o)
。如果 ArrayList
存储的是基本类型的包装类,比如 Integer
,那么在调用 remove()
方法时需要注意自动装箱可能带来的问题。例如,list.remove(1)
可能不是删除元素 1
,而是删除索引为 1
的元素。
空指针:如果 ArrayList
中存储的是对象,那么在删除元素时,如果 ArrayList
中存在 null
,需要注意 NullPointerException
异常。
解答:在遍历 ArrayList
时移除元素,需要注意 ConcurrentModificationException
异常。以下是几种常见的移除元素的方法:
Iterator
的 remove()
方法。这是最安全且推荐的方式。Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (需要移除的条件) {
iterator.remove();
}
}
List
的 remove()
方法,但需要倒序遍历。for (int i = list.size() - 1; i >= 0; i--) {
if (需要移除的条件) {
list.remove(i);
}
}
Collection.removeIf()
方法。list.removeIf(item -> 需要移除的条件);
以上三种方法都可以在遍历 ArrayList
时移除元素,但是推荐使用 Iterator
的 remove()
方法,因为它在面对并发修改时可以提供正确的行为。
解答:ArrayList
是非线程安全的,它的方法没有进行同步处理,所以在多线程环境下可能会出现问题。
如果需要在多线程环境下使用 ArrayList
,有以下几种方式可以保证线程安全:
Collections.synchronizedList()
方法创建一个同步的 ArrayList
:List<String> list = Collections.synchronizedList(new ArrayList<>());
这个方法会返回一个线程安全的 List
,所有的方法都进行了同步处理。
CopyOnWriteArrayList
,这是一个线程安全的 List
实现:List<String> list = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList
在每次修改操作(如添加、删除元素)时,都会复制一份新的数组,这样可以避免修改操作对遍历操作的影响,提供了一种读写分离的机制。
使用并发包 java.util.concurrent
中的其他线程安全集合,如 ConcurrentLinkedQueue
、ConcurrentHashMap
等。
使用 synchronized
关键字或 Lock
对象手动同步 ArrayList
的操作。
需要注意的是,以上方法在提供线程安全的同时,可能会带来一定的性能开销。
解答:Array
和 ArrayList
是 Java 中两种不同的数据结构,它们的主要区别在于大小的可变性、性能、类型限制和功能。
Array
是固定长度的,一旦创建,其大小就不能改变。ArrayList
是动态的,可以自动调整其大小以适应元素的添加和删除。Array
在访问元素时具有更好的性能,因为它是基于索引的数据结构。ArrayList
在添加和删除元素时具有更好的性能,特别是在列表的末尾,因为它可以动态调整大小。Array
可以存储基本数据类型或对象。ArrayList
只能存储对象,不能直接存储基本数据类型。Array
是一个简单的数据结构,没有提供很多功能。ArrayList
是一个集合类,提供了大量的方法,如添加、删除、遍历等。总的来说,Array
和 ArrayList
各有优势,选择哪种取决于具体的需求。
解答:LinkedList
是 Java 中常用的一种链表实现,其底层是基于双向链表实现的。
存储结构:LinkedList
内部使用一个双向链表来存储元素。每个元素(节点)都包含了对前一个元素和后一个元素的引用。
插入和删除:LinkedList
在链表头部和尾部插入和删除元素非常高效,时间复杂度为 O(1)。在链表中间插入和删除元素需要先找到对应的位置,时间复杂度为 O(n)。
访问元素:LinkedList
不支持高效的随机访问,访问特定索引的元素需要从头(或尾)开始遍历,时间复杂度为 O(n)。
线程安全:LinkedList
是非线程安全的,如果需要在多线程环境下使用,可以使用 Collections.synchronizedList()
方法返回一个线程安全的 LinkedList
。
总的来说,LinkedList
是一种链表结构,适合插入和删除操作频繁的场景,但在访问元素时效率较低。
解答:ArrayList
和 LinkedList
都是 List
接口的实现,但它们在内部数据结构和性能上有一些区别:
ArrayList
是基于动态数组实现的,支持随机访问,按索引访问元素非常快,时间复杂度为 O(1)。LinkedList
是基于双向链表实现的,不支持高效的随机访问,按索引访问元素需要从头(或尾)开始遍历,时间复杂度为 O(n)。ArrayList
的插入和删除操作需要进行数组元素的移动(除非插入和删除操作在列表末尾进行),所以插入和删除元素的时间复杂度为 O(n)。LinkedList
的插入和删除操作只需要改变节点的引用,所以在列表中间插入和删除元素的时间复杂度为 O(1)(前提是已经获取到了要插入位置的节点)。ArrayList
的内存占用相对较低,因为它只需要存储元素数据和数组的引用。LinkedList
的内存占用较高,因为它需要额外存储节点之间的引用。总的来说,ArrayList
更适合随机访问场景,LinkedList
更适合插入和删除操作频繁的场景。
解答:Vector
是 Java 中的一种线程安全的动态数组实现,其底层是基于数组实现的。
存储结构:Vector
内部使用一个数组(elementData
)来存储元素。当添加元素时,如果数组已满,就会创建一个新的更大的数组,并将原数组的内容复制到新数组中,这个过程称为扩容。
扩容机制:Vector
的扩容机制是,每次扩容时,新数组的大小是原数组大小的 2 倍。如果这个值仍然不足以满足需求,那么新数组的大小就直接设置为需求的大小。
插入和删除:Vector
在尾部插入和删除元素非常高效,时间复杂度为 O(1)。但是在中间或头部插入和删除元素需要移动大量元素,时间复杂度为 O(n)。
访问元素:由于底层是数组,所以 Vector
支持随机访问,按索引访问元素的时间复杂度为 O(1)。
线程安全:Vector
的所有公共方法都进行了同步处理,所以它是线程安全的。但这也意味着在单线程环境下,Vector
的性能会比 ArrayList
差一些。
总的来说,Vector
是一种线程安全的动态数组结构,适合在多线程环境下使用,但在单线程环境下,由于同步处理带来的开销,其性能会比 ArrayList
差一些。
解答:ArrayList
和 Vector
都是实现了 List
接口的类,它们都是基于动态数组实现的,但是在同步性和性能上有一些区别:
Vector
是线程安全的,它的大部分方法都进行了同步处理,可以在多线程环境下使用。实现上其实就是 Vector 在 ArrayList 的方法前面加上了 Synchronized。ArrayList
是非线程安全的,它的方法没有进行同步处理,所以在多线程环境下可能会出现问题。Vector
进行了同步处理,所以在单线程环境下,Vector
的性能会比 ArrayList
差一些。ArrayList
在单线程环境下的性能比 Vector
好,因为它没有进行同步处理。ArrayList
在每次需要扩容时,都会增加到原来的 1.5 倍。Vector
在每次需要扩容时,都会增加到原来的 2 倍。总的来说,如果需要在多线程环境下使用,可以选择 Vector
,如果是在单线程环境下,或者已经通过其他方式处理了同步问题,那么 ArrayList
会是更好的选择。
解答:Stack
是 Java 中的一种后进先出(LIFO)的数据结构,其底层是基于 Vector
实现的。
存储结构:Stack
内部使用一个 Vector
来存储元素。当添加元素(压栈)时,元素被添加到 Vector
的末尾;当删除元素(弹栈)时,元素从 Vector
的末尾被移除。
扩容机制:由于 Stack
是基于 Vector
实现的,所以其扩容机制与 Vector
相同。每次扩容时,新数组的大小是原数组大小的 2 倍。
插入和删除:Stack
的插入和删除操作都在 Vector
的末尾进行,所以非常高效,时间复杂度为 O(1)。
访问元素:Stack
提供了 peek()
方法来查看栈顶元素,时间复杂度为 O(1)。由于底层是 Vector
,所以 Stack
也支持按索引访问元素,但这并不常用。
线程安全:由于 Stack
是基于 Vector
实现的,所以它也是线程安全的。但这也意味着在单线程环境下,Stack
的性能会比基于 ArrayList
实现的 Deque
差一些。
总的来说,Stack
是一种后进先出的数据结构,适合在需要后进先出操作的场景下使用,如函数调用栈、回溯算法等。
解答:队列(Queue)和栈(Stack)是两种常见的数据结构,它们在数据的存储和访问方式上有一些区别。
队列(Queue):队列是一种先进先出(FIFO,First In First Out)的数据结构,新元素添加到队列的尾部,而移除元素则从队列的头部开始。队列常用于实现需要按照元素添加顺序进行处理的场景,如任务队列、消息队列等。
栈(Stack):栈是一种后进先出(LIFO,Last In First Out)的数据结构,新元素添加到栈的顶部,移除元素也从栈的顶部开始。栈常用于实现需要后进先出操作的场景,如函数调用栈、撤销操作、深度优先搜索等。
总的来说,队列和栈的主要区别在于元素的访问顺序:队列是先进先出,而栈是后进先出。
Queue
和 Deque
是 Java 中的两种接口,分别代表队列和双端队列这两种数据结构。
Queue
:队列是一种先进先出(FIFO)的数据结构,支持在队尾插入元素(offer
方法),在队头删除元素(poll
方法),查看队头元素(peek
方法)。常用的 Queue
实现类有 LinkedList
、PriorityQueue
等。
Deque
:双端队列是一种特殊的队列,它支持在两端插入和删除元素。除了支持 Queue
的所有操作外,还支持在队头插入元素(offerFirst
方法),在队尾删除元素(pollLast
方法),查看队尾元素(peekLast
方法)。常用的 Deque
实现类有 ArrayDeque
、LinkedList
等。
在实际使用时,可以根据具体需求选择使用 Queue
还是 Deque
,以及选择合适的实现类。例如,如果需要按元素的优先级进行处理,可以使用 PriorityQueue
;如果需要在两端插入和删除元素,可以使用 Deque
。
解答:PriorityQueue
是 Java 中的一种特殊的队列,它的特点是队列中的元素按照它们的优先级进行排序。
存储结构:PriorityQueue
内部使用一个二叉小顶堆来实现。二叉小顶堆是一种特殊的二叉树,树中任意一个非叶子节点的值都不大于其子节点的值,树的根节点(顶部)是所有节点中的最小值。
元素排序:PriorityQueue
中的元素可以自然排序,或者通过提供的 Comparator
进行自定义排序。在添加元素时,会根据元素的优先级找到合适的位置保证堆的性质。
插入和删除:插入元素和删除元素(或者说是获取并删除最高优先级的元素)的时间复杂度都是 O(log n)。
访问元素:PriorityQueue
提供了 peek
方法来访问最高优先级的元素(堆顶元素),时间复杂度为 O(1)。
线程安全:PriorityQueue
是非线程安全的,如果需要在多线程环境下使用,可以使用 PriorityBlockingQueue
。
总的来说,PriorityQueue
是一种可以动态调整内部元素顺序的队列,适合实现需要动态排序的场景,如任务调度、Dijkstra 算法、Huffman 编码等。
解答:BlockingQueue
是 Java 中的一个接口,它是一种特殊的队列,主要提供了阻塞操作的支持,适用于生产者消费者模式。
阻塞操作:BlockingQueue
提供了 put
和 take
方法,当队列满时,put
方法会阻塞直到队列不满;当队列空时,take
方法会阻塞直到队列不空。这样可以简化生产者消费者模式的实现,无需显式地进行同步和唤醒操作。
非阻塞操作:BlockingQueue
也提供了非阻塞操作 offer
和 poll
,如果无法立即执行操作,这些方法会返回一个特殊值(如 null
或 false
)而不是阻塞。
超时操作:BlockingQueue
还提供了带超时的 offer
和 poll
方法,如果在指定的时间内无法执行操作,这些方法会返回一个特殊值。
实现类:BlockingQueue
的常用实现类有 ArrayBlockingQueue
、LinkedBlockingQueue
、PriorityBlockingQueue
、SynchronousQueue
等,它们分别适用于不同的场景。
总的来说,BlockingQueue
是一种支持阻塞操作的队列,适合实现生产者消费者模式,可以简化多线程编程。