第16章 集合框架:List 与 Set
数组长度固定,无法满足动态需求。集合(Collection)是 Java 提供的"可动态增长"的容器。本节重点学习 List 与 Set 两大接口及常用实现。
一、集合框架概览
先看数组的痛点------长度一旦确定就改不了:String[] arr = new String[3] 只能放 3 个元素,第 4 个直接 ArrayIndexOutOfBoundsException。集合就是为了解决"长度动态变化"而生的:想加就加、想删就删,不用管容量。整个集合框架分两大阵营:
Collection(单列集合)
├── List 有序、可重复
│ ├── ArrayList 底层数组,查询快增删慢
│ ├── LinkedList 底层链表,增删快查询慢
│ └── Vector 线程安全(老式,已少用)
├── Set 无序(部分有序)、不可重复
│ ├── HashSet 底层 HashMap,无序
│ ├── LinkedHashSet 底层 LinkedHashMap,按插入顺序
│ └── TreeSet 底层红黑树,自动排序
└── Queue 队列(先进先出)
Map(双列集合,键值对)→ 下一节讲
最常用的三个实现:ArrayList、HashSet、HashMap,记住"List 有序可重复、Set 无序不可重复"。
二、List 接口


List 是有序集合,元素可重复,可以通过下标访问。它有两个关键特性:元素按插入顺序排列 、每个元素有下标------这决定了它"像数组一样好查"。
ArrayList(最常用)
底层是可变数组,查询快、增删慢(中间插入要移动元素):
java
import java.util.ArrayList;
import java.util.List;
List<String> list = new ArrayList<>();
list.add("Java"); // 追加元素
list.add("Python");
list.add("Go");
list.add(1, "C++"); // 在指定下标插入
System.out.println(list); // [Java, C++, Python, Go]
System.out.println(list.size()); // 4
System.out.println(list.get(0)); // Java:按下标取
System.out.println(list.contains("Go")); // true
list.remove(0); // 按下标删除
list.remove("Go"); // 按内容删除
为什么"查询快、增删慢"?数组在内存里是连续空间 ,按下标直接定位(O(1));而中间插入/删除要整体"挪位置"(O(n))。查找还有 indexOf(x) / lastIndexOf(x)(第一次/最后一次出现的位置),找不到返回 -1。
LinkedList
底层是双向链表,增删快、查询慢:
java
LinkedList<String> linkedList = new LinkedList<>();
linkedList.addFirst("头"); // 头部添加
linkedList.addLast("尾"); // 尾部添加
linkedList.removeFirst(); // 删除头部
LinkedList 实现了 Deque 接口,常用作栈或队列:
java
// 当栈用(后进先出)
Deque<String> stack = new LinkedList<>();
stack.push("A");
stack.push("B");
System.out.println(stack.pop()); // B
System.out.println(stack.peek()); // A:只看栈顶
// 当队列用(先进先出)
Queue<String> queue = new LinkedList<>();
queue.offer("1号");
queue.offer("2号");
System.out.println(queue.poll()); // 1号
选择建议
- 大多数场景(遍历、按下标查)→ ArrayList
- 频繁在头部/中间增删 → LinkedList
实际开发里 90% 场景用 ArrayList 就够,LinkedList 更多用在队列、栈。
三、遍历 List 的四种方式

java
// JDK 11 和 17 里可以直接用 List.of 快速创建不可变列表
// (JDK 8 没有这个写法,要用 Arrays.asList 或手动 add)
List<String> list = List.of("Java", "Python", "Go");
// 方式一:普通 for(需要下标)
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// 方式二:增强 for(最常用)
for (String lang : list) {
System.out.println(lang);
}
// 方式三:Iterator 迭代器
Iterator<String> it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
// 方式四:Lambda(JDK 8+,简洁)
list.forEach(lang -> System.out.println(lang));
增强 for 遍历时不能同时修改集合 (会抛 ConcurrentModificationException),要删除元素用 Iterator 的
remove()或 for 倒序删除。
四、Set 接口

Set 是不可重复的集合,不能按下标访问。Set 的"去重"能力靠的是哈希,先看最常用的 HashSet。
HashSet(最常用)
底层是 HashMap,无序(不保证迭代顺序),去重核心:
java
Set<String> set = new HashSet<>();
set.add("apple");
set.add("banana");
set.add("apple"); // 重复元素:添加失败,不报错
System.out.println(set); // [banana, apple](顺序不固定)
System.out.println(set.size()); // 2:去重成功
为什么"无序"?元素存到哪个位置由 hashCode() 算出的哈希桶决定,跟插入顺序无关,别依赖迭代顺序。
去重的原理
HashSet 判断元素是否重复:先看 hashCode(),hashCode 相同再看 equals()。
因此:自定义类的对象放进 HashSet,必须同时重写 hashCode() 和 equals(),否则两个内容相同的对象会被当成不同元素。
java
class Student {
String name;
int age;
Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Student)) return false;
Student s = (Student) o;
return age == s.age && name.equals(s.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age); // IDEA 可自动生成
}
}
java
Set<Student> students = new HashSet<>();
students.add(new Student("张三", 18));
students.add(new Student("张三", 18)); // 重写后被认为是重复,去重成功
System.out.println(students.size()); // 1
如果不重写会怎样? 用 Object 默认的 hashCode/equals(比较内存地址),两个 new 出来的对象地址不同,被当成两个元素(见易错点 3 的演示)。
LinkedHashSet 与 TreeSet
java
// LinkedHashSet:按插入顺序,可去重且有序
Set<String> lhs = new LinkedHashSet<>();
lhs.add("c"); lhs.add("a"); lhs.add("b");
System.out.println(lhs); // [c, a, b]:保持插入顺序
// TreeSet:自动排序(元素必须可比较)
Set<Integer> ts = new TreeSet<>();
ts.add(5); ts.add(1); ts.add(3);
System.out.println(ts); // [1, 3, 5]:升序
TreeSet 要求元素实现 Comparable 接口,或构造时传入比较器:
java
// 自定义比较器:按年龄升序
Set<Student> byAge = new TreeSet<>((s1, s2) -> Integer.compare(s1.age, s2.age));
byAge.add(new Student("张三", 20));
byAge.add(new Student("李四", 18));
for (Student s : byAge) {
System.out.println(s.name + ": " + s.age); // 李四: 18 / 张三: 20
}
三个 Set 怎么选?只要去重用 HashSet,去重又保序用 LinkedHashSet,自动排序用 TreeSet。
五、集合元素去重实战
保留 List 中的不重复元素:
java
List<String> list = Arrays.asList("a", "b", "a", "c", "b");
Set<String> unique = new HashSet<>(list); // 利用 Set 去重
System.out.println(unique); // [a, b, c]
六、集合工具类 Collections

Collections 是操作集合的静态工具类,排序、反转、打乱、查找全都有:
java
List<Integer> nums = new ArrayList<>(Arrays.asList(3, 1, 4, 1, 5));
Collections.sort(nums); // 排序
Collections.reverse(nums); // 反转
Collections.shuffle(nums); // 随机打乱
Collections.max(nums); // 最大值
Collections.min(nums); // 最小值
Collections.frequency(nums, 1); // 统计出现次数
七、扩展知识
1. ArrayList 的扩容机制
ArrayList 底层的数组是"装不满"的------它有容量(capacity)和实际大小(size)。当 size 达到容量上限时会自动扩容 :新容量约为旧容量的 1.5 倍 (JDK 8/11/17 都是 oldCapacity + (oldCapacity >> 1)),用 Arrays.copyOf 把旧数组整体拷贝 到新数组。默认初始容量是 10,前 10 个元素不扩容,第 11 个才触发。
经验:频繁 add 大量数据时,用
new ArrayList<>(预估容量)能显著减少扩容拷贝的开销。
2. LinkedList vs ArrayList 终极对比
| 维度 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 连续数组 | 双向链表 |
| 按下标查询 get(i) | O(1),直接定位 | O(n),要挨个找 |
| 头部插入/删除 | O(n),整体挪动 | O(1),改指针 |
| 中间插入/删除 | O(n),挪动后半段 | O(n),先找到位置 |
| 额外内存 | 少 | 每个节点多存前后指针 |
| 典型场景 | 遍历、随机访问 | 频繁头尾增删、队列/栈 |
结论:LinkedList 遍历要用迭代器/增强 for,别用 get(i)------get(i) 每次都从头遍历,整体 O(n²)。
3. HashSet 底层就是 HashMap
打开 HashSet 源码会发现它内部维护了一个 HashMap:add 的元素作为 key 存入,value 统一用占位对象 PRESENT。元素唯一性 = key 唯一性,复用 HashMap 的去重逻辑。
set.add(x) 的返回值就是"这次有没有真正加进去":
java
Set<String> set = new HashSet<>();
System.out.println(set.add("a")); // true:加进去了
System.out.println(set.add("a")); // false:已存在,加失败
4. 迭代器与 fail-fast 机制
集合内部维护修改计数器 modCount,每次 add/remove 都会 +1。迭代器创建时记录当时的 modCount,迭代中一旦发现计数变了,立即抛 ConcurrentModificationException------这就是 fail-fast(快速失败):
java
List<String> list = new ArrayList<>();
list.add("a"); list.add("b"); list.add("c");
// ❌ 错误示范:迭代中调用 list.add
for (String s : list) {
list.add("x"); // 抛 ConcurrentModificationException
}
正确的删除姿势见易错点 1:用迭代器自己的 remove()(it.remove() 会同步维护 modCount,所以安全)。
八、易错点
1. 遍历时删除元素 → 抛异常
java
// ❌ 增强 for 中删除:抛 ConcurrentModificationException
List<String> list = new ArrayList<>();
list.add("a"); list.add("b"); list.add("c");
for (String s : list) {
if (s.equals("b")) list.remove(s);
}
// ✅ 用迭代器的 remove()
List<String> list2 = new ArrayList<>();
list2.add("a"); list2.add("b"); list2.add("c");
Iterator<String> it = list2.iterator();
while (it.hasNext()) {
if (it.next().equals("b")) it.remove();
}
System.out.println(list2); // [a, c]
// ✅ 或者 for 循环倒序删除(见易错点 5)
2. List.of 创建的列表不能增删
java
// 需 JDK 11/17 才能运行(JDK 8 没有 List.of)
List<String> fixed = List.of("a", "b", "c");
// ❌ fixed.add("d"); // 抛 UnsupportedOperationException
// ❌ fixed.remove("a"); // 同样抛异常
// ✅ 想要可变列表,先拷贝一份
List<String> mutable = new ArrayList<>(List.of("a", "b", "c"));
mutable.add("d");
System.out.println(mutable); // [a, b, c, d]
顺带:JDK 8 里常用的 Arrays.asList 也是定长的------不能 add/remove(抛 UnsupportedOperationException),但可以 set 修改已有元素。
3. HashSet 存自定义对象没重写 hashCode/equals
java
// ❌ 没重写:两个"张三"被当成不同元素,都存进去了
class StudentNoHash {
String name;
StudentNoHash(String name) { this.name = name; }
}
Set<StudentNoHash> s1 = new HashSet<>();
s1.add(new StudentNoHash("张三"));
s1.add(new StudentNoHash("张三"));
System.out.println(s1.size()); // 2:去重失败!
// ✅ 重写 hashCode + equals 之后(见上文的 Student 类)
Set<Student> s2 = new HashSet<>();
s2.add(new Student("张三", 18));
s2.add(new Student("张三", 18));
System.out.println(s2.size()); // 1:去重成功
4. 用下标访问 LinkedList,性能惨不忍睹
java
// ❌ LinkedList.get(i) 每次都要从头遍历,整体 O(n²)
for (int i = 0; i < linkedList.size(); i++) {
System.out.println(linkedList.get(i));
}
// ✅ 迭代器 / 增强 for:O(n)
for (String s : linkedList) {
System.out.println(s);
}
5. 正序删除会漏删(下标前移)
java
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "b", "c"));
// ❌ 正序删除"b":删掉第一个 b 后元素前移,第二个 b 被跳过
for (int i = 0; i < list.size(); i++) {
if (list.get(i).equals("b")) list.remove(i);
}
System.out.println(list); // [a, b, c]:漏删了一个 b!
// ✅ 倒序删除:不涉及下标前移问题
for (int i = list.size() - 1; i >= 0; i--) {
if (list.get(i).equals("b")) list.remove(i);
}
System.out.println(list); // [a, c]
九、小结
- List 有序可重复(ArrayList 查询快 / LinkedList 增删快)
- Set 不可重复(HashSet 无序 / LinkedHashSet 保序 / TreeSet 排序)
- 自定义类进 HashSet 必须重写 hashCode + equals
- 遍历集合别在增强 for 中修改集合,删除用 Iterator.remove() 或倒序 for
- ArrayList 自动扩容(默认容量 10,1.5 倍增长),大数据量提前指定容量
- LinkedList 遍历要用迭代器,别用 get(i);JDK 11/17 的 List.of 不可变、不能增删
下一节学习集合框架:Map。
下一篇:第17章 集合框架:Map