摘要: 本文系统讲解Java集合框架中List和Set两大核心接口,深入剖析ArrayList、LinkedList、HashSet、LinkedHashSet、TreeSet等常用实现类的特性、使用场景与性能差异。通过对比数组痛点、集合体系结构、遍历方式及元素相等性判断,帮助开发者掌握集合框架的核心概念与实战应用,并介绍JDK 21的Sequenced Collections新特性,为构建高效Java应用奠定基础。
关键词: Java集合框架, List接口, Set接口, ArrayList, HashSet
恭喜你完成了第三阶段的“学生管理系统”实战项目。在那个项目里,我们大量使用了 ArrayList——用来存放学生、老师、课程、成绩记录。你可能已经感受到了:ArrayList 比数组好用太多了。
- 数组:大小固定,一旦创建就不能改变。你没法往一个满的数组里加新元素,除非新建一个更大的数组,再把旧数据复制过去。
ArrayList:大小是动态的,你可以随便往里面加元素,它自己会扩容。你还可以随时删除、插入、查找……方便得像一个“智能容器”。
这种能自动伸缩、能随意增删改查的容器,在 Java 的世界里有一个统称——集合(Collection)。
从今天开始,我们要花几篇文章系统地学习 Java 集合框架(Collections Framework)。这是 JDK 中最重要的类库之一,几乎每个程序都会用到它。今天我们先来认识其中最常用的两大家族:List 和 Set。
1. 为什么需要集合?—— 数组的痛点
在学集合之前,我们得先理解:数组到底哪里不够用?
让我们回顾一下数组的几个“硬伤”:
// 数组的痛点
String[] names = new String[10]; // 必须指定大小,而且不能变
names[0] = "张三";
// 如果我想加第 11 个人,怎么办?—— 得新建一个更大的数组,复制旧数据
数组的局限性:
- 长度固定:创建后无法动态扩展。
- 删除/插入麻烦:删除中间一个元素,需要手动移动后面所有元素。
- 操作少:没有直接的方法来排序、查找、判断是否包含某个元素。
- 类型单一:只能存同一种类型(不过这是优点,泛型集合也这样)。
而集合框架就是为了解决这些问题而设计的。它提供了一系列可动态扩展、功能丰富、类型安全的数据结构,让你能轻松地管理一组对象。
2. 集合框架体系概览 —— 一张“家族树”
Java 集合框架的核心是两大接口:
Collection:表示一组对象的集合(单个元素的容器)。Map:表示一组“键值对”的集合(我们下一篇会讲)。
Collection 接口又有三个主要的子接口:
Collection (接口)
├── List (接口) —— 有序、可重复
│ ├── ArrayList (类)
│ ├── LinkedList (类)
│ └── Vector (类,历史遗留,不推荐)
├── Set (接口) —— 无序、不可重复
│ ├── HashSet (类)
│ ├── TreeSet (类)
│ └── LinkedHashSet (类)
└── Queue (接口) —— 队列(FIFO)
├── LinkedList (也实现了 Queue)
└── PriorityQueue (类)
今天,我们重点讲 List 和 Set,它们是最常用的两个分支。
3. List 接口 —— 有序的、可重复的“列表”
List 的特点是:
- 有序:元素按照你插入的顺序排列,你可以通过索引(位置)访问每个元素。
- 可重复:允许存储相同的元素(即
equals()返回true的两个元素可以同时存在)。
我们最常用的两个 List 实现是 ArrayList 和 LinkedList。
3.1 ArrayList —— 基于数组的动态列表
ArrayList 内部使用一个动态数组来存储元素。它支持快速随机访问(通过索引取元素很快),但在中间插入或删除元素时,需要移动后面的元素,效率较低。
创建和使用 ArrayList:
import java.util.ArrayList;
import java.util.List;
public class ArrayListDemo {
public static void main(String[] args) {
// 创建一个 ArrayList,默认初始容量为 10(但你可以不关心这个)
List<String> names = new ArrayList<>();
// 添加元素
names.add("张三");
names.add("李四");
names.add("王五");
names.add("张三"); // 可以重复
// 通过索引访问
System.out.println("索引0:" + names.get(0)); // 张三
// 获取大小
System.out.println("大小:" + names.size()); // 4
// 插入元素到指定位置(索引1处插入"赵六")
names.add(1, "赵六");
// 删除元素(按索引)
names.remove(2); // 删除索引2的元素(原"李四")
// 删除元素(按对象)
names.remove("张三"); // 只删除第一个匹配的
// 判断是否包含
System.out.println("是否包含王五:" + names.contains("王五")); // true
// 遍历方式1:增强 for
for (String name : names) {
System.out.println(name);
}
// 遍历方式2:带索引的 for
for (int i = 0; i < names.size(); i++) {
System.out.println(i + ": " + names.get(i));
}
}
}
ArrayList 的特点总结:
- 适合频繁随机访问(
get()、set())的场景。 - 不适合频繁在中间插入或删除,因为需要移动大量元素。
- 尾部添加(
add())效率高,因为大多数情况下不需要移动元素。
3.2 LinkedList —— 基于链表的动态列表
LinkedList 内部使用双向链表来存储元素。它的每个元素都存储了对前一个和后一个元素的引用。因此,在列表中间插入或删除元素非常高效(只需要修改相邻节点的引用),但随机访问较慢(需要从头或尾遍历)。
import java.util.LinkedList;
import java.util.List;
public class LinkedListDemo {
public static void main(String[] args) {
// LinkedList 也实现了 List 接口,所以可以用 List 引用
List<String> names = new LinkedList<>();
names.add("张三");
names.add("李四");
names.addFirst("王五"); // LinkedList 特有方法:在头部添加
names.addLast("赵六"); // 在尾部添加
// 可以用同样的 List 方法
System.out.println(names.get(0)); // 王五
System.out.println(names.size()); // 4
// 删除第一个元素
names.remove(0);
// 还可以用队列操作(因为 LinkedList 也实现了 Queue 接口)
// 这个我们后面讲 Queue 时会看到
}
}
LinkedList 的特点总结:
- 适合频繁在头部或中间插入/删除的场景。
- 随机访问较慢(
get(index)需要遍历)。 - 实现了
Deque接口,可以作为栈或队列使用。
3.3 ArrayList vs LinkedList —— 怎么选?
| 场景 | 推荐 |
|---|---|
| 经常通过索引访问元素 | ArrayList |
| 经常在尾部添加/删除元素 | ArrayList |
| 经常在头部或中间插入/删除元素 | LinkedList |
| 需要作为队列或栈使用 | LinkedList(它实现了 Deque) |
| 遍历所有元素(不按索引) | 两者差不多 |
在实际开发中,ArrayList 的使用频率远高于 LinkedList,因为大多数场景下我们主要是遍历和尾部添加。除非你明确知道需要频繁在中间插入,否则默认选 ArrayList。
4. Set 接口 —— 无序的、不可重复的“集合”
Set 的特点是:
- 无序:元素的顺序不保证(具体取决于实现,
HashSet无序,TreeSet按自然顺序排序,LinkedHashSet按插入顺序)。 - 不可重复:不允许包含重复的元素(即
equals()返回true的两个元素不能同时存在)。
Set 就像数学中的集合概念:去重、不关心顺序。
最常用的 Set 实现有 HashSet、LinkedHashSet 和 TreeSet。
4.1 HashSet —— 基于哈希表的集合
HashSet 内部使用 HashMap 来存储元素(键),它通过元素的 hashCode() 和 equals() 来判断是否重复。
import java.util.HashSet;
import java.util.Set;
public class HashSetDemo {
public static void main(String[] args) {
Set<String> fruits = new HashSet<>();
fruits.add("苹果");
fruits.add("香蕉");
fruits.add("橘子");
fruits.add("苹果"); // 重复,不会添加
System.out.println("大小:" + fruits.size()); // 3
System.out.println("是否包含香蕉:" + fruits.contains("香蕉")); // true
// 遍历顺序不保证(每次可能不同)
for (String fruit : fruits) {
System.out.println(fruit);
}
// 可能的输出:橘子、苹果、香蕉(顺序不定)
}
}
HashSet 特点:
- 基于哈希表,查找、添加、删除的平均时间复杂度为 O(1)。
- 不保证顺序。
- 元素必须正确重写
hashCode()和equals()(对于 JDK 类如String已经做好的)。
4.2 LinkedHashSet —— 保留插入顺序的 HashSet
LinkedHashSet 是 HashSet 的子类,它在哈希表的基础上增加了一个链表来维护插入顺序。
Set<String> orderedSet = new LinkedHashSet<>();
orderedSet.add("苹果");
orderedSet.add("香蕉");
orderedSet.add("橘子");
// 遍历顺序永远是:苹果、香蕉、橘子(插入顺序)
4.3 TreeSet —— 基于红黑树的有序集合
TreeSet 内部使用红黑树(一种自平衡二叉搜索树)来存储元素,因此它会对元素进行自然排序(或通过自定义比较器排序)。
import java.util.Set;
import java.util.TreeSet;
public class TreeSetDemo {
public static void main(String[] args) {
Set<Integer> numbers = new TreeSet<>();
numbers.add(5);
numbers.add(1);
numbers.add(10);
numbers.add(3);
// 遍历时会按照自然顺序(升序)输出
for (int num : numbers) {
System.out.print(num + " "); // 1 3 5 10
}
}
}
TreeSet 特点:
- 元素有序(默认自然升序,可通过
Comparator定制)。 - 基于红黑树,查找、添加、删除的时间复杂度为 O(log n)。
- 元素必须实现
Comparable接口(或者传入Comparator)。
4.4 Set 的选择指南
| 需求 | 推荐 |
|---|---|
| 只关心去重,不关心顺序 | HashSet |
| 需要去重,且要保留插入顺序 | LinkedHashSet |
| 需要去重,且要按某种顺序排序 | TreeSet |
5. 集合的遍历 —— 三种方式
遍历集合中的元素是最高频的操作之一。Java 提供了三种主流方式:
① 增强 for 循环(最常用)
List<String> list = new ArrayList<>();
// ... 添加元素
for (String item : list) {
System.out.println(item);
}
适用于所有实现了 Iterable 接口的集合(Collection 都实现了)。
② 普通 for 循环 + 索引(仅 List)
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
适用于需要知道索引或修改元素的情况。
③ 迭代器(Iterator)
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String item = it.next();
System.out.println(item);
// 可以在遍历时安全删除元素:it.remove()
}
迭代器是早期 Java 的通用遍历方式,现在增强 for 更简洁,但迭代器在需要删除元素时很有用。
④ JDK 8+ 的 forEach 方法(Lambda)
list.forEach(item -> System.out.println(item));
// 或使用方法引用
list.forEach(System.out::println);
这是函数式风格的遍历,我们会在 Lambda 和 Stream 那篇详细讲。
6. 集合中元素相等性的判断 —— equals() 和 hashCode()
在使用 List 的 contains()、remove(Object o) 以及所有 Set 操作时,Java 会调用元素的 equals() 方法来判断两个元素是否相等。
对于自定义类,如果你希望在集合中去重或判断包含,必须重写 equals() 和 hashCode()。
public class Person {
private String id;
private String name;
public Person(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return id.equals(person.id); // 以 id 为唯一标识
}
@Override
public int hashCode() {
return id.hashCode();
}
// getter/setter...
}
如果没有重写,equals() 默认比较引用地址,两个内容相同的对象会被视为不同,Set 就会允许重复,contains() 也会返回 false。
💡 对于
String、Integer等 JDK 类,它们已经正确重写了equals()和hashCode(),所以可以直接使用。
7. JDK 21 的新特性:Sequenced Collections(有序集合)
在 JDK 21 中,引入了一个新的接口族:SequencedCollection、SequencedSet、SequencedMap。它们为有序集合提供了统一的方法来访问“第一个”和“最后一个”元素。
在此之前,List 有 get(0) 和 get(size()-1),但 Set(如 LinkedHashSet)没有直接的方法获取第一个或最后一个元素。JDK 21 统一了这些操作。
// 在 JDK 21 中,LinkedHashSet 实现了 SequencedSet
import java.util.LinkedHashSet;
import java.util.SequencedSet;
public class SequencedDemo {
public static void main(String[] args) {
SequencedSet<String> set = new LinkedHashSet<>();
set.add("A");
set.add("B");
set.add("C");
// 新方法:获取第一个和最后一个
System.out.println(set.getFirst()); // A
System.out.println(set.getLast()); // C
// 反转顺序
SequencedSet<String> reversed = set.reversed();
for (String s : reversed) {
System.out.print(s + " "); // C B A
}
}
}
SequencedCollection 为 List、Deque 和有序 Set 提供了统一的操作接口,让代码更一致、更清晰。
8. 综合示例 —— 在“学生管理系统”中使用集合
在上一篇文章的学生管理系统中,我们已经在用 List 了。现在让我们用 Set 来增强它:确保同一个学生不能重复选同一门课(我们之前用 enrolledCourses.contains(course) 检查,但 contains 对 List 是 O(n) 遍历,如果我们用 Set 存选修课程,那 contains 就是 O(1) 了,而且自动去重)。
我们修改 Student 类中的 enrolledCourses 为 Set<Course>:
// 之前用 List
// private List<Course> enrolledCourses = new ArrayList<>();
// 改为 Set(使用 HashSet,不关心顺序)
private Set<Course> enrolledCourses = new HashSet<>();
// 选课方法
public void enroll(Course course) throws DuplicateEnrollmentException {
if (enrolledCourses.contains(course)) {
throw new DuplicateEnrollmentException(studentId, course.getCourseId());
}
enrolledCourses.add(course);
course.addStudent(this);
}
// 获取选修课程列表(返回不可修改的 Set,或复制一份)
public Set<Course> getEnrolledCourses() {
return new HashSet<>(enrolledCourses);
}
这样改之后,contains 检查更快,而且 Set 保证了自动去重,即使不小心调用了两次 add,也不会重复添加。
9. 常见陷阱与注意事项
ArrayList的remove(int index)vsremove(Object o):如果你传入的是一个int,它会被当作索引;如果想删除值为某个整数的对象,需要传入Integer.valueOf(num)。Set的元素不可重复依赖于equals()和hashCode():自定义类必须正确重写这两个方法。- 遍历时修改集合:使用增强
for遍历时,不能直接调用remove(),否则会抛出ConcurrentModificationException。可以使用迭代器的remove()或使用removeIf()方法(JDK 8+)。 TreeSet要求元素可比较:如果元素没有实现Comparable,构造TreeSet时需要传入Comparator。- 集合的初始容量:
ArrayList和HashMap等都有初始容量参数,如果你能预估元素数量,指定初始容量可以减少扩容开销,提升性能。
10. 今天的总结
今天我们初步接触了 Java 集合框架:
- 集合框架 解决了数组长度固定、操作不便的问题,提供了丰富的数据结构。
List有序、可重复,常用实现ArrayList(随机访问快)和LinkedList(插入删除快)。Set无序、不可重复,常用实现HashSet(最快、无序)、LinkedHashSet(保留插入顺序)、TreeSet(自动排序)。- 遍历方式:增强
for、普通for(仅List)、迭代器、forEach(Lambda)。 - 元素相等性依赖
equals()和hashCode(),自定义类必须重写。 - JDK 21 新增了
SequencedCollection接口,统一了有序集合的getFirst()、getLast()、reversed()等操作。
集合框架的内容非常丰富,今天你先掌握 List 和 Set 的基本用法。下一篇文章,我们将重点讲 Map——它存储键值对,是另一种极其重要的集合类型。
动手试试
- 创建一个
ArrayList,添加 10 个随机整数,然后删除所有偶数(提示:用迭代器或removeIf)。 - 创建一个
HashSet,添加一些字符串,观察遍历顺序是否与插入顺序一致。然后改用LinkedHashSet,再观察遍历顺序。 - 创建一个
TreeSet,添加几个字符串,观察它们是否按字典序排序。然后传入一个Comparator实现降序排序。 - 定义一个
Book类,包含title和author,重写equals()和hashCode()使得两本书只要标题和作者相同就视为相等。然后用HashSet存储几本书,看看重复的书是否能被正确去重。 - 修改学生管理系统的
Student类,把enrolledCourses改为Set<Course>,并修改相关方法,确保编译通过且功能正常。
集合是 Java 程序员的“日常工具”,多用多练,你会越来越熟练。我们下一篇见。😃
📌 获取本系列示例代码请访问 GitCode。
:List 与 Set&spm=1001.2101.3001.5002&articleId=162907373&d=1&t=3&u=3c0cce00860844a69688b8d77641ade9)
9654

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



