Java集合框架中的Set、List和Queue
在Java编程中,集合框架是非常重要的一部分,它提供了各种数据结构来存储和操作数据。本文将详细介绍Java集合框架中的Set、List和Queue接口及其实现类。
1. Set和SortedSet
Set接口继承自Collection接口,它提供了更具体的方法契约,但自身并没有添加新的方法。Set集合不包含重复元素,如果尝试将相同元素添加两次,第一次添加会返回
true
,第二次则返回
false
。同样,移除元素时,如果尝试移除同一个元素两次,第一次移除会返回
true
,因为集合发生了改变,第二次则返回
false
,因为元素已不存在。Set集合最多可以包含一个
null
元素。
SortedSet接口继承自Set接口,它指定了一个额外的契约:迭代器会按照指定顺序返回元素,默认是元素的自然顺序。在
java.util
包提供的SortedSet实现中,也可以指定一个
Comparator
对象来对元素进行排序。
SortedSet接口添加了一些在有序集合中有意义的方法:
-
public Comparator<? super E> comparator()
:返回当前使用的
Comparator
,如果使用的是元素的自然顺序,则返回
null
。
-
public E first()
:返回集合中的第一个(最小的)对象。
-
public E last()
:返回集合中的最后一个(最大的)对象。
-
public SortedSet<E> subSet(E min, E max)
:返回一个视图,包含集合中所有大于等于
min
且小于
max
的元素。该视图由原集合支持,对原集合在该范围内的修改会反映在返回的子集中,反之亦然。如果
min
大于
max
,或者该集合本身是另一个集合的视图,而
min
或
max
超出了该视图的范围,会抛出
IllegalArgumentException
。如果尝试修改返回的集合,使其包含超出指定范围的元素,也会抛出该异常。
-
public SortedSet<E> headSet(E max)
:返回一个视图,包含集合中所有小于
max
的元素。该视图由原集合支持,抛出的异常与
subSet
方法相同。
-
public SortedSet<E> tailSet(E min)
:与
headSet
类似,但返回的集合包含所有大于等于
min
的元素。
下面是一个创建视图快照的示例代码:
public <T> SortedSet<T> copyHead(SortedSet<T> set, T max) {
SortedSet<T> head = set.headSet(max);
return new TreeSet<T>(head); // contents from head
}
java.util
包提供了两种通用的Set实现:
HashSet
和
LinkedHashSet
,以及一种SortedSet实现:
TreeSet
。
1.1 HashSet
HashSet
是使用哈希表实现的Set集合。修改
HashSet
的内容或检查元素是否存在的操作时间复杂度为O(1),即这些操作的时间不会随着集合大小的增加而增加(假设元素的
hashCode
方法实现良好,能够将哈希码均匀分布在
int
值的整个范围内)。
HashSet
有四个构造函数:
-
public HashSet(int initialCapacity, float loadFactor)
:创建一个具有指定初始容量和负载因子的新
HashSet
。负载因子必须是正数,当集合中元素数量与哈希桶数量的比例大于或等于负载因子时,哈希桶的数量会增加。
-
public HashSet(int initialCapacity)
:创建一个具有指定初始容量和默认负载因子的新
HashSet
。
-
public HashSet()
:创建一个具有默认初始容量和负载因子的新
HashSet
。
-
public HashSet(Collection<? extends E> coll)
:创建一个新的
HashSet
,其初始内容为指定集合中的元素。初始容量基于指定集合的大小,使用默认负载因子。
迭代
HashSet
所需的时间与集合大小和容量之和成正比。
1.2 LinkedHashSet
LinkedHashSet
继承自
HashSet
,它通过定义元素的顺序来细化了
HashSet
的契约。迭代
LinkedHashSet
时,元素将按照插入顺序返回。除了这一点,
LinkedHashSet
的行为与
HashSet
相同,并且定义了相同形式的构造函数。由于维护链表结构的开销,
LinkedHashSet
的性能可能比
HashSet
稍慢,但迭代只需要与集合大小成正比的时间,与容量无关。
1.3 TreeSet
如果需要一个有序集合,可以使用
TreeSet
,它将元素存储在一个平衡树结构中。这意味着修改或搜索树所需的时间复杂度为O(log n)。
TreeSet
有四个构造函数:
-
public TreeSet()
:创建一个按照元素自然顺序排序的新
TreeSet
。所有添加到该集合的元素必须实现
Comparable
接口,并且相互可比。
-
public TreeSet(Collection<? extends E> coll)
:相当于先创建一个
TreeSet
,然后将指定集合中的元素添加到该集合中。
-
public TreeSet(Comparator<? super E> comp)
:创建一个按照指定
Comparator
排序的新
TreeSet
。
-
public TreeSet(SortedSet<E> set)
:创建一个新的
TreeSet
,其初始内容与指定的
SortedSet
相同,并且按照相同的方式排序。
2. List
List接口继承自Collection接口,它定义了一个元素有顺序的集合,每个元素在集合中都有一个特定的位置,索引从0到
list.size() - 1
。换句话说,List定义了一个元素序列。这需要对从Collection继承的几个方法的契约进行细化:添加元素时,元素将被放置在列表的末尾;移除列表中的第
n
个元素时,其后的元素会向前移动,成为新的第
n
个元素;
toArray
方法会按照列表的顺序填充数组。
List接口还添加了一些在有序集合中有意义的方法:
-
public E get(int index)
:返回列表中指定索引处的元素。
-
public E set(int index, E elem)
:将列表中指定索引处的元素设置为
elem
,并返回原来的元素(可选操作)。
-
public void add(int index, E elem)
:将元素
elem
添加到列表的指定索引处,将该位置及之后的所有元素向后移动一位(可选操作)。
-
public E remove(int index)
:移除并返回列表中指定索引处的元素,将该位置之后的所有元素向前移动一位(可选操作)。
-
public int indexOf(Object elem)
:返回列表中第一个等于
elem
的元素的索引,如果
elem
为
null
,则返回
null
对应的索引。如果没有找到匹配的元素,返回
-1
。
-
public int lastIndexOf(Object elem)
:返回列表中最后一个等于
elem
的元素的索引,如果
elem
为
null
,则返回
null
对应的索引。如果没有找到匹配的元素,返回
-1
。
-
public List<E> subList(int min, int max)
:返回一个视图,包含列表中从
min
(包含)到
max
(不包含)的元素。返回的列表由原列表支持,对返回列表的修改会反映在原列表中。直接对原列表的修改不一定会在子列表中可见,可能会导致未定义的结果(因此不建议这样做)。子列表允许对列表的一部分进行与整个列表相同的操作,因此是一个强大的工具。例如,可以使用
list.subList(min, max).clear()
移除列表的一部分。
-
public ListIterator<E> listIterator(int index)
:返回一个
ListIterator
对象,从列表的指定索引处开始迭代元素。
-
public ListIterator<E> listIterator()
:返回一个
ListIterator
对象,从列表的开头开始迭代元素。
所有接受索引的方法,如果索引小于0或大于等于列表的大小,都会抛出
IndexOutOfBoundsException
。
java.util
包提供了两种List实现:
ArrayList
和
LinkedList
。
2.1 ArrayList
ArrayList
是一个基本的列表实现,它将元素存储在一个底层数组中。在列表末尾添加和移除元素非常简单,时间复杂度为O(1)。获取指定位置的元素的时间复杂度也是O(1)。但在列表中间添加和移除元素的代价较高,时间复杂度为O(n - i),其中
n
是列表的大小,
i
是要移除元素的位置。添加或移除元素需要将数组的剩余部分向上或向下移动一位。
ArrayList
有一个容量,即它可以容纳的元素数量,在不分配新的更大数组的情况下。随着元素的添加,它们会存储在数组中,但当空间不足时,必须分配一个替换数组。正确设置初始容量可以提高性能。如果数据的初始大小明显小于最终大小,将初始容量设置为较大的值可以减少底层数组需要替换为更大副本的次数。但将容量设置得太大可能会浪费空间。
ArrayList
有三个构造函数:
-
public ArrayList()
:创建一个具有默认容量的新
ArrayList
。
-
public ArrayList(int initialCapacity)
:创建一个初始可以存储
initialCapacity
个元素而无需调整大小的新
ArrayList
。
-
public ArrayList(Collection<? extends E> coll)
:创建一个新的
ArrayList
,其初始内容为指定集合中的元素。数组的初始容量为指定集合大小的110%,以允许在不调整大小的情况下进行一些增长。元素的顺序由集合的迭代器返回。
ArrayList
还提供了一些管理容量的方法:
-
public void trimToSize()
:将容量设置为列表的当前大小。如果当前容量大于大小,将分配一个新的、更小的底层数组,并将当前值复制到其中。这样可以减少存储列表所需的内存量,但会有一定的代价。
-
public void ensureCapacity(int minCapacity)
:如果当前容量小于
minCapacity
,则将容量设置为
minCapacity
。如果即将向列表中添加大量元素,可以使用此方法确保数组最多只重新分配一次(在调用
ensureCapacity
时),而不是在添加元素时可能多次重新分配。
2.2 LinkedList
LinkedList
是一个双向链表,其性能特点与
ArrayList
几乎相反:在列表末尾添加元素的时间复杂度为O(1),但其他操作则相反。在列表中间添加或移除元素的时间复杂度为O(1),因为不需要复制元素,而获取指定位置
i
的元素的时间复杂度为O(i),因为需要从一端开始遍历列表到第
i
个元素。
LinkedList
提供了两个构造函数,并添加了一些对双向链表有用且高效的方法:
-
public LinkedList()
:创建一个新的空
LinkedList
。
-
public LinkedList(Collection<? extends E> coll)
:创建一个新的
LinkedList
,其初始内容为指定集合中的元素。元素的顺序由集合的迭代器返回。
-
public E getFirst()
:返回列表中的第一个对象。
-
public E getLast()
:返回列表中的最后一个对象。
-
public E removeFirst()
:移除列表中的第一个对象。
-
public E removeLast()
:移除列表中的最后一个对象。
-
public void addFirst(E elem)
:将
elem
添加到列表的开头。
-
public void addLast(E elem)
:将
elem
添加到列表的末尾。
LinkedList
是实现队列的良好基础,实际上它实现了Queue接口。对于栈或在发现元素时构建元素列表,
ArrayList
更高效,因为它需要的对象更少:一个数组而不是列表中每个元素一个对象。还可以通过简单地使用一个
int
作为索引来高效地扫描
ArrayList
,而无需创建
Iterator
对象。这是在需要频繁扫描列表时使用
ArrayList
的一个很好的理由。
下面是一个存储多边形顶点的
Polygon
类示例:
import java.util.List;
import java.util.ArrayList;
public class Polygon {
private List<Point> vertices =
new ArrayList<Point>();
public void add(Point p) {
vertices.add(p);
}
public void remove(Point p) {
vertices.remove(p);
}
public int numVertices() {
return vertices.size();
}
// ... other methods ...
}
注意,
vertices
是一个
List
引用,被赋值为一个
ArrayList
对象。应该尽可能将变量声明为抽象类型,优先选择抽象的
List
类型而不是具体的实现类
ArrayList
。这样,如果使用
LinkedList
更高效,可以只更改创建列表的那一行代码,而其他代码保持不变。
2.3 RandomAccess Lists
RandomAccess
标记接口用于标记支持快速随机访问的列表实现。随机访问意味着可以直接访问列表中的任何元素。例如,
ArrayList
实现了
RandomAccess
接口,因为可以通过索引轻松访问任何元素。相反,
LinkedList
没有实现
RandomAccess
接口,因为通过索引访问元素需要从列表的一端开始遍历。一些操作随机访问列表的算法在应用于顺序列表时性能较差,因此
RandomAccess
接口的目的是允许算法根据所处理的列表类型进行调整。一般来说,如果以下代码通常比使用迭代器更快:
for (int i = 0; i < list.size(); i++)
process(list.get(i));
那么你的列表应该实现
RandomAccess
接口。
总结
本文介绍了Java集合框架中的Set、List和Queue接口及其实现类。Set集合不包含重复元素,SortedSet是有序的Set集合。List集合是元素有顺序的集合,提供了通过索引访问元素的方法。Queue集合定义了一个头部位置,通常遵循先进先出的顺序。不同的实现类在性能和使用场景上有所不同,开发者可以根据具体需求选择合适的集合类。
以下是一个简单的对比表格:
| 集合类型 | 实现类 | 特点 | 适用场景 |
| ---- | ---- | ---- | ---- |
| Set | HashSet | 基于哈希表,不保证元素顺序,插入、查找和删除操作时间复杂度为O(1) | 需要快速查找元素,不关心元素顺序 |
| Set | LinkedHashSet | 继承自HashSet,保证元素插入顺序 | 需要保持元素插入顺序 |
| Set | TreeSet | 基于红黑树,元素按自然顺序或指定比较器排序 | 需要元素有序 |
| List | ArrayList | 基于数组,随机访问快,插入和删除操作在末尾快,中间慢 | 需要频繁随机访问元素,插入和删除操作主要在末尾 |
| List | LinkedList | 基于双向链表,插入和删除操作快,随机访问慢 | 需要频繁插入和删除元素,随机访问较少 |
| Queue | LinkedList | 实现了Queue接口,支持队列操作 | 实现队列或栈 |
| Queue | PriorityQueue | 基于优先堆,元素按优先级排序 | 需要根据元素优先级进行处理 |
下面是一个简单的mermaid流程图,展示了选择合适集合类的基本流程:
graph TD;
A[是否需要元素有序?] -->|是| B[是否需要随机访问?];
A -->|否| C[是否需要保持插入顺序?];
B -->|是| D[使用TreeSet或ArrayList];
B -->|否| E[使用TreeSet或LinkedList];
C -->|是| F[使用LinkedHashSet];
C -->|否| G[使用HashSet];
通过理解这些集合类的特点和适用场景,开发者可以更好地选择和使用Java集合框架,提高代码的性能和可维护性。
3. Queue
Queue接口继承自Collection接口,它为集合的内部组织添加了一些结构。队列定义了一个头部位置,即下一个要移除的元素。队列通常按照先进先出(FIFO)的顺序操作,但也可以是后进先出(LIFO,通常称为栈),或者由比较器或可比较元素定义特定的顺序。每个实现都必须指定其排序属性。
Queue接口添加了几个专门处理头部的方法:
-
public E element()
:返回但不移除队列的头部元素。如果队列为空,会抛出
NoSuchElementException
。
-
public E peek()
:返回但不移除队列的头部元素。如果队列为空,返回
null
。此方法与
element
的区别仅在于对空队列的处理。
-
public E remove()
:返回并移除队列的头部元素。如果队列为空,会抛出
NoSuchElementException
。
-
public E poll()
:返回并移除队列的头部元素。如果队列为空,返回
null
。此方法与
remove
的区别仅在于对空队列的处理。
还有一个用于插入元素到队列的方法:
-
public boolean offer(E elem)
:尝试将给定元素插入此队列。如果插入成功,返回
true
,否则返回
false
。对于有合理理由拒绝请求的队列(如容量有限的队列),此方法比
Collection
的
add
方法更可取,因为
add
方法只能通过抛出异常来表示失败。
一般来说,队列不应该接受
null
元素,因为
null
被用作
poll
和
peek
方法表示空队列的哨兵值。
LinkedList
类提供了最简单的Queue实现。由于历史原因,
LinkedList
类接受
null
元素,但在将
LinkedList
实例用作队列时,应避免插入
null
元素。
3.1 PriorityQueue
另一种Queue实现是
PriorityQueue
,它是一个基于优先堆的无界队列。队列的头部是其中最小的元素,最小元素由元素的自然顺序或提供的比较器决定。
PriorityQueue
一般不是一个排序队列,不能将其传递给期望排序集合的方法,因为
iterator
返回的迭代器不保证按优先级顺序遍历元素,而是保证从队列中移除元素是按给定顺序进行的,迭代器可以按任何顺序遍历元素。如果需要有序遍历,可以将元素提取到数组中,然后对数组进行排序。
最小元素代表最高还是最低“优先级”取决于自然顺序或比较器的定义。例如,如果根据线程的执行优先级对
Thread
对象进行排队,那么最小元素代表执行优先级最低的线程。
PriorityQueue
的性能特征未明确指定。基于优先堆的良好实现对头部的操作时间复杂度为O(1),一般插入操作的时间复杂度为O(log n)。任何需要遍历的操作,如移除特定元素或搜索元素,时间复杂度为O(n)。
PriorityQueue
有六个构造函数:
-
public PriorityQueue(int initialCapacity)
:创建一个可以存储
initialCapacity
个元素而无需调整大小的新
PriorityQueue
,元素按自然顺序排序。
-
public PriorityQueue()
:创建一个具有默认初始容量的新
PriorityQueue
,元素按自然顺序排序。
-
public PriorityQueue(int initialCapacity, Comparator<? super E> comp)
:创建一个初始可以存储
initialCapacity
个元素而无需调整大小的新
PriorityQueue
,元素按提供的比较器排序。
-
public PriorityQueue(Collection<? extends E> coll)
:创建一个新的
PriorityQueue
,其初始内容为指定集合中的元素。队列的初始容量为指定集合大小的110%,以允许在不调整大小的情况下进行一些增长。如果指定集合是
SortedSet
或另一个
PriorityQueue
,则此队列的排序方式相同;否则,元素将按自然顺序排序。如果指定集合中的任何元素无法比较,会抛出
ClassCastException
。
-
public PriorityQueue(SortedSet<? extends E> coll)
:创建一个新的
PriorityQueue
,其初始内容为指定
SortedSet
中的元素。队列的初始容量为指定集合大小的110%,以允许在不调整大小的情况下进行一些增长。此队列的排序方式与指定
SortedSet
相同。
-
public PriorityQueue(PriorityQueue<? extends E> coll)
:创建一个新的
PriorityQueue
,其初始内容为指定
PriorityQueue
中的元素。队列的初始容量为指定集合大小的110%,以允许在不调整大小的情况下进行一些增长。此队列的排序方式与指定
PriorityQueue
相同。
由于
PriorityQueue
不接受
null
元素,所有接受集合的构造函数在遇到
null
元素时会抛出
NullPointerException
。可以使用
comparator
方法检索用于构造优先队列的比较器,该方法与
SortedSet
中的
comparator
方法具有相同的契约,如果使用元素的自然排序,则返回
null
。
不同集合类的操作复杂度对比
| 集合类型 | 添加元素 | 移除元素 | 查找元素 | 随机访问 |
|---|---|---|---|---|
| HashSet | O(1) | O(1) | O(1) | 不支持 |
| LinkedHashSet | O(1) | O(1) | O(1) | 不支持 |
| TreeSet | O(log n) | O(log n) | O(log n) | 不支持 |
| ArrayList | 末尾:O(1),中间:O(n) | 末尾:O(1),中间:O(n) | O(n) | O(1) |
| LinkedList | 末尾:O(1),中间:O(1) | 末尾:O(1),中间:O(1) | O(n) | O(n) |
| PriorityQueue | O(log n) | O(log n) | O(n) | 不支持 |
选择合适集合类的详细流程
graph LR;
A[确定需求] --> B[是否允许重复元素?];
B -->|是| C[是否需要元素有序?];
B -->|否| D[使用Set集合];
C -->|是| E[是否需要随机访问?];
C -->|否| D;
E -->|是| F[使用TreeSet或ArrayList];
E -->|否| G[使用TreeSet或LinkedList];
D --> H[是否需要保持插入顺序?];
H -->|是| I[使用LinkedHashSet];
H -->|否| J[使用HashSet];
F --> K[插入和删除操作主要位置?];
K -->|末尾| L[优先使用ArrayList];
K -->|中间| M[考虑LinkedList];
G --> N[操作类型?];
N -->|频繁插入删除| M;
N -->|其他| G1[根据具体情况选择TreeSet或LinkedList];
M --> O[是否实现Queue接口?];
O -->|是| P[使用LinkedList];
O -->|否| M;
J --> Q[是否需要队列功能?];
Q -->|是| R[使用PriorityQueue或LinkedList];
Q -->|否| J;
示例代码演示不同集合类的使用
HashSet示例
import java.util.HashSet;
import java.util.Set;
public class HashSetExample {
public static void main(String[] args) {
Set<String> hashSet = new HashSet<>();
hashSet.add("apple");
hashSet.add("banana");
hashSet.add("apple"); // 重复元素,不会添加
System.out.println(hashSet);
}
}
LinkedList作为Queue示例
import java.util.LinkedList;
import java.util.Queue;
public class LinkedListQueueExample {
public static void main(String[] args) {
Queue<String> queue = new LinkedList<>();
queue.offer("element1");
queue.offer("element2");
System.out.println(queue.poll()); // 移除并返回头部元素
System.out.println(queue.peek()); // 返回头部元素但不移除
}
}
总结
Java集合框架提供了丰富的集合类,包括Set、List和Queue及其各种实现。不同的集合类在元素存储方式、操作复杂度和适用场景上各有特点。在实际开发中,需要根据具体需求,如是否允许重复元素、是否需要元素有序、是否需要随机访问等,来选择合适的集合类。通过合理选择集合类,可以提高代码的性能和可维护性。例如,如果需要快速查找元素且不关心顺序,可选择
HashSet
;如果需要频繁在列表中间进行插入和删除操作,
LinkedList
可能更合适;如果需要元素按优先级处理,
PriorityQueue
是不错的选择。同时,要注意不同集合类对
null
元素的处理和性能特点,以避免潜在的问题。
Java集合框架Set、List和Queue详解
超级会员免费看
1254

被折叠的 条评论
为什么被折叠?



