12、数据结构与算法 - 基础:常用集合简述
一、Java 集合框架全景图
Java 集合框架(Java Collections Framework,JCF)是 JDK 1.2 引入的一套统一数据结构体系,它将所有常用数据容器纳入统一的接口层级,并提供了算法工具类和线程安全包装。整个框架以两大根接口为起点分叉:
dart
Iterable
|
┌──────┴──────┐
Collection Map
/ | \ / \
List Queue Set HashMap SortedMap
/ | \ / \ |
ArrayList Deque HashSet ... ConcurrentHashMap
LinkedList | / \
Vector ArrayDeque LinkedHashSet
| TreeSet
Stack
Collection 接口代表一组对象的集合,细分为三个子接口:
- List:有序、可重复、按索引访问
- Set:无序、不可重复
- Queue / Deque:按特定规则进出(FIFO / LIFO / 优先级)
Map 接口 代表键值对的映射,键不可重复,每个键映射到一个值。注意:Map 并不继承 Collection,它是独立的层级。
二、List 家族:有序可重复的线性列表
2.1 核心实现类对比
| 特性 | ArrayList | LinkedList | Vector | Stack |
|---|---|---|---|---|
| 底层数据结构 | 动态数组(Object[]) | 双向链表 | 动态数组 | 继承 Vector |
| 随机访问(get) | O(1) | O(N)(需遍历) | O(1) | O(1) |
| 头/尾插入 | O(N) / 均摊 O(1) | O(1) / O(1) | O(N) / 均摊 O(1) | O(N) / 均摊 O(1) |
| 中间插入/删除 | O(N)(移动元素) | O(1)(修改指针,但查找 O(N)) | O(N) | O(N) |
| 内存占用 | 仅数据 + 少量预留 | 数据 + 两个指针(prev/next) | 同 ArrayList | 同 Vector |
| 线程安全 | ❌ | ❌ | ✅(synchronized) | ✅ |
| 扩容策略 | 1.5 倍(oldCapacity + oldCapacity >> 1) | 按需分配节点 | 2 倍(可指定增量) | 同 Vector |
| 适用场景 | 随机访问多、尾部增删多 | 频繁头尾操作、无随机访问需求 | 旧代码遗留,极少新用 | 需要 LIFO 栈结构 |
2.2 ArrayList 的动态扩容机制
java
/**
* 模拟 ArrayList 的核心扩容逻辑
* 演示容量增长和元素拷贝过程
*/
public class ArrayListSimulator<T> {
private Object[] elements;
private int size;
private static final int DEFAULT_CAPACITY = 10;
public ArrayListSimulator() {
elements = new Object[DEFAULT_CAPACITY];
}
public void add(T element) {
ensureCapacity(size + 1);
elements[size++] = element;
}
/** 扩容:新容量 = 旧容量 + 旧容量/2(即 1.5 倍) */
private void ensureCapacity(int minCapacity) {
if (minCapacity > elements.length) {
int oldCapacity = elements.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5 倍
if (newCapacity < minCapacity) {
newCapacity = minCapacity;
}
elements = Arrays.copyOf(elements, newCapacity);
}
}
@SuppressWarnings("unchecked")
public T get(int index) {
if (index >= size) throw new IndexOutOfBoundsException();
return (T) elements[index];
}
public int size() { return size; }
// 测试
public static void main(String[] args) {
ArrayListSimulator<String> list = new ArrayListSimulator<>();
for (int i = 0; i < 25; i++) {
list.add("元素" + i);
}
System.out.println("添加25个元素后 size = " + list.size());
System.out.println("索引0: " + list.get(0));
System.out.println("索引24: " + list.get(24));
// 边界测试
try {
list.get(99);
} catch (IndexOutOfBoundsException e) {
System.out.println("越界异常捕获成功");
}
}
}
2.3 LinkedList 的双向链表特性
LinkedList 同时实现了 List 和 Deque 两个接口,这意味着它既是一个可索引的列表,又是一个双端队列。底层是带哨兵节点的双向链表,头部和尾部操作均为 O(1)。但由于没有维护索引结构,get(index) 需要从头部(或尾部,JDK 会选较近的一端)遍历,实际为 O(N)。
三、Set 家族:不可重复的无序/有序集合
3.1 核心实现类对比
| 特性 | HashSet | LinkedHashSet | TreeSet |
|---|---|---|---|
| 底层实现 | HashMap(键存元素,值存 dummy Object) | LinkedHashMap | TreeMap(红黑树) |
| 元素顺序 | 无序(取决于哈希分布) | 插入顺序(链表维护) | 自然排序或 Comparator 排序 |
| 查找/插入/删除 | O(1) 平均 | O(1) 平均 | O(log N) |
| null 元素 | ✅ 允许一个 | ✅ 允许一个 | ❌ 不允许(需要比较) |
| 内存开销 | 中等(HashMap 内部结构) | 较高(额外链表指针) | 较高(树节点 + 颜色标记) |
| 适用场景 | 通用去重 | 需要保持插入顺序的去重 | 需要有序遍历的去重 |
3.2 HashSet 去重原理剖析
HashSet 的去重依赖于两个方法:hashCode() 和 equals()。当添加一个元素时,流程如下:
- 计算元素的
hashCode(),定位到对应的哈希桶 - 若桶为空,直接插入成功
- 若桶非空,遍历桶内元素,调用
equals()逐一比较 - 若有
equals()返回 true 的元素,视为重复,添加失败 - 若无匹配,追加到桶链表(或红黑树节点)中
重写 equals 必须同时重写 hashCode ,这是 Java 对象相等性的"契约"。两个对象若
equals为 true,其hashCode必须相同;反之则不一定。违反此约定会导致 HashSet/HashMap 出现"幽灵重复"------两个逻辑相等的对象被视为不同。
java
import java.util.*;
/**
* HashSet 去重原理演示
* 包含正确重写 equals/hashCode 的示例类
*/
public class HashSetDedupDemo {
/** 正确重写了 equals 和 hashCode 的学生类 */
static class Student {
int id;
String name;
Student(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Student)) return false;
Student other = (Student) o;
return id == other.id; // 以 id 判断是否同一学生
}
@Override
public int hashCode() {
return Objects.hash(id); // 与 equals 保持一致
}
@Override
public String toString() {
return "Student{id=" + id + ", name='" + name + "'}";
}
}
/** 错误示范:只重写 equals,没有重写 hashCode */
static class BadStudent {
int id;
BadStudent(int id) { this.id = id; }
@Override
public boolean equals(Object o) {
if (!(o instanceof BadStudent)) return false;
return id == ((BadStudent) o).id;
}
// 未重写 hashCode → 默认使用 Object 的 hashCode(基于内存地址)
}
public static void main(String[] args) {
// 正确示例
Set<Student> set1 = new HashSet<>();
set1.add(new Student(1, "张三"));
set1.add(new Student(1, "张三改")); // 同 id,视为重复
set1.add(new Student(2, "李四"));
System.out.println("正确去重: " + set1);
// 输出: [Student{id=1, name='张三'}, Student{id=2, name='李四'}]
// 注意:第二个 id=1 的元素被拒绝
// 错误示例:未重写 hashCode 导致去重失败
Set<BadStudent> set2 = new HashSet<>();
set2.add(new BadStudent(100));
set2.add(new BadStudent(100)); // 本应去重,但因 hashCode 不同而重复添加
System.out.println("错误示范(set大小应为1): " + set2.size()); // 输出 2
// LinkedHashSet:保持插入顺序
Set<String> linkedSet = new LinkedHashSet<>();
linkedSet.add("Cherry");
linkedSet.add("Banana");
linkedSet.add("Apple");
System.out.println("LinkedHashSet(插入顺序): " + linkedSet);
// [Cherry, Banana, Apple]
// TreeSet:自然排序
Set<String> treeSet = new TreeSet<>();
treeSet.add("Cherry");
treeSet.add("Banana");
treeSet.add("Apple");
System.out.println("TreeSet(字典序): " + treeSet);
// [Apple, Banana, Cherry]
}
}
四、Map 家族:键值对的统治
4.1 核心实现类全方位对比
| 特性 | HashMap | LinkedHashMap | TreeMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|---|---|
| 底层结构 | 数组 + 链表 + 红黑树 | HashMap + 双向链表 | 红黑树 | 数组 + 链表 | Node 数组 + CAS + synchronized |
| 键值顺序 | 无序 | 插入顺序 / 访问顺序 | 键自然排序或 Comparator | 无序 | 无序 |
| null 键/值 | ✅ 允许 | ✅ 允许 | ❌ 键不允许 / ✅ 值允许 | ❌ 均不允许 | ❌ 均不允许 |
| 线程安全 | ❌ | ❌ | ❌ | ✅(方法级锁) | ✅(细粒度锁,高并发) |
| 默认容量 | 16 | 16 | --- | 11 | 16 |
| 装载因子 | 0.75 | 0.75 | --- | 0.75 | 0.75 |
| 扩容倍数 | 2× | 2× | --- | 2×+1 | 2× |
| JDK 版本 | 1.2 | 1.4 | 1.2 | 1.0(遗留) | 1.5(JUC) |
| 推荐使用 | ✅ | ✅(需顺序时) | ✅(需排序时) | ❌ 不推荐 | ✅(并发场景) |
4.2 TreeMap 的红黑树排序之旅
java
import java.util.*;
/**
* TreeMap 排序演示
* 展示自然排序(Comparable)与自定义排序(Comparator)
*/
public class TreeMapSortDemo {
/** 自定义比较器:按字符串长度排序,等长按字母序 */
static class LengthComparator implements Comparator<String> {
@Override
public int compare(String s1, String s2) {
int lenDiff = s1.length() - s2.length();
return lenDiff != 0 ? lenDiff : s1.compareTo(s2);
}
}
public static void main(String[] args) {
// 默认自然排序(字典序)
TreeMap<String, Integer> defaultMap = new TreeMap<>();
defaultMap.put("dog", 1);
defaultMap.put("apple", 2);
defaultMap.put("cat", 3);
defaultMap.put("banana", 4);
System.out.println("自然排序: " + defaultMap.keySet());
// [apple, banana, cat, dog]
// 自定义排序(按长度)
TreeMap<String, Integer> customMap = new TreeMap<>(new LengthComparator());
customMap.put("dog", 1);
customMap.put("apple", 2);
customMap.put("cat", 3);
customMap.put("banana", 4);
System.out.println("按长度排序: " + customMap.keySet());
// [cat, dog, apple, banana] ------ 注意等长的 dog/cat 按字母序
// 获取子映射
System.out.println("headMap('dog'): " + defaultMap.headMap("dog"));
// {apple=2, banana=4, cat=3}
System.out.println("tailMap('dog'): " + defaultMap.tailMap("dog"));
// {dog=1}
// 最值操作
System.out.println("firstKey: " + defaultMap.firstKey()); // apple
System.out.println("lastKey: " + defaultMap.lastKey()); // dog
// 边界测试:空 TreeMap
TreeMap<String, Integer> emptyMap = new TreeMap<>();
try {
emptyMap.firstKey();
} catch (NoSuchElementException e) {
System.out.println("空TreeMap获取firstKey抛异常(预期行为)");
}
}
}
五、Queue / Deque 家族:先进先出与双端操作
| 特性 | LinkedList(作队列用) | ArrayDeque | PriorityQueue |
|---|---|---|---|
| 底层 | 双向链表 | 循环数组 | 二叉堆(小顶堆) |
| FIFO 操作 | O(1) | O(1) 均摊 | O(log N)(出队) |
| 双向操作 | ✅ Deque | ✅ Deque | ❌ 仅单向 |
| null 元素 | ✅ | ❌ | ❌ |
| 线程安全 | ❌ | ❌ | ❌ |
| 适用场景 | 频繁头尾操作 | 高性能栈/队列(推荐替代 Stack 和 LinkedList) | 优先队列(任务调度、Top K) |
最佳实践:用 ArrayDeque 替代 Stack(Stack 已过时)和用作 FIFO 队列的 LinkedList(ArrayDeque 更省内存且更快,因为无链表指针开销且缓存友好)。
六、线程安全方案选择指南
| 方案 | 原理 | 性能 | 适用场景 |
|---|---|---|---|
Vector / Hashtable |
synchronized 方法锁 | 低(竞争时严重退化) | 不推荐(遗留类) |
Collections.synchronizedXXX() |
装饰器模式,全表锁 | 低 | 低并发、简单场景 |
ConcurrentHashMap |
JDK 7 分段锁 → JDK 8 CAS+synchronized | 高(细粒度,非阻塞读) | 高并发读写 |
CopyOnWriteArrayList |
写时复制数组 | 读极快,写很慢 | 读多写极少(如监听器列表) |
ConcurrentLinkedQueue |
CAS 无锁队列 | 高 | 高并发生产者-消费者 |
BlockingQueue(Array/Linked) |
ReentrantLock + Condition | 中 | 生产者-消费者,线程池任务队列 |
七、集合选用速查表
按场景需求快速定位合适的集合类:
| 你的需求 | 推荐集合 | 理由 |
|---|---|---|
| 简单存储,随机访问多 | ArrayList |
O(1) 索引 |
| 频繁头尾增删 | LinkedList 或 ArrayDeque |
O(1) 头尾操作 |
| 需要栈(LIFO) | ArrayDeque |
比 Stack 快且现代 |
| 需要队列(FIFO) | ArrayDeque 或 LinkedList |
两者均实现了 Queue |
| 去重,不关心顺序 | HashSet |
O(1) 增删查 |
| 去重,保持插入顺序 | LinkedHashSet |
O(1) + 有序遍历 |
| 去重,需要排序 | TreeSet |
O(log N) + 有序 |
| 键值映射,高性能 | HashMap |
O(1) 平均 |
| 键值映射,需顺序 | LinkedHashMap |
O(1) + 有序遍历(可实现 LRU) |
| 键值映射,需排序 | TreeMap |
O(log N) + 有序键 |
| 高并发键值映射 | ConcurrentHashMap |
线程安全 + 高性能 |
| 读多写少的 List | CopyOnWriteArrayList |
无锁读 |
| 优先级处理 | PriorityQueue |
二叉堆,O(log N) |
| 线程安全的生产消费 | ArrayBlockingQueue / LinkedBlockingQueue |
阻塞队列 |
八、集合框架的迭代器与 fail-fast 机制
Java 集合的迭代器(Iterator)在遍历时会检测结构性修改(非迭代器自身的 add/remove 操作)。若检测到并发修改(modCount 变化),会立即抛出 ConcurrentModificationException,这种策略称为 fail-fast(快速失败)。
java
import java.util.*;
/**
* fail-fast 机制演示
*/
public class FailFastDemo {
public static void main(String[] args) {
// 正面示例:使用迭代器的 remove 安全删除
List<String> list1 = new ArrayList<>(
Arrays.asList("A", "B", "C", "D"));
Iterator<String> it1 = list1.iterator();
while (it1.hasNext()) {
String s = it1.next();
if ("B".equals(s)) {
it1.remove(); // 迭代器自身的 remove,安全
}
}
System.out.println("安全删除后: " + list1); // [A, C, D]
// 反面示例:遍历中直接修改集合 → ConcurrentModificationException
List<String> list2 = new ArrayList<>(
Arrays.asList("X", "Y", "Z"));
try {
for (String s : list2) { // for-each 底层也是迭代器
if ("Y".equals(s)) {
list2.remove(s); // 直接修改集合,触发 fail-fast
}
}
} catch (ConcurrentModificationException e) {
System.out.println("捕获到 ConcurrentModificationException(预期)");
}
// 正确替代方案 1:使用 removeIf(Java 8+)
List<String> list3 = new ArrayList<>(
Arrays.asList("X", "Y", "Z"));
list3.removeIf(s -> "Y".equals(s));
System.out.println("removeIf 后: " + list3); // [X, Z]
// 正确替代方案 2:使用 CopyOnWriteArrayList(无 fail-fast)
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>(
Arrays.asList("1", "2", "3"));
for (String s : cowList) {
if ("2".equals(s)) {
cowList.remove(s); // 不会抛异常,但效率低
}
}
System.out.println("CopyOnWriteArrayList 删除后: " + cowList); // [1, 3]
}
}
九、Comparable 与 Comparator:排序的两种姿势
| 特性 | Comparable | Comparator |
|---|---|---|
| 所属包 | java.lang | java.util |
| 实现方法 | compareTo(T o) |
compare(T o1, T o2) |
| 实现位置 | 在待比较的类内部实现 | 在外部独立定义 |
| 耦合度 | 高(侵入类定义) | 低(解耦,策略模式) |
| 排序方式 | 单一(自然顺序) | 多种(可定义多个比较器) |
| 典型用法 | String、Integer 等 JDK 内置类 | 第三方类或多种排序需求 |
| 调用方式 | Collections.sort(list) |
Collections.sort(list, comparator) |
优先使用
Comparator------ 更灵活、低耦合,符合开闭原则。数据类本身不应该承担排序策略。
十、总结
Java 集合框架是数据结构理论在工业级编程语言中的集大成体现:
- List 解决有序存储问题,Array 和 Linked 两种底层实现覆盖了随机访问与频繁修改两大类场景
- Set 解决去重问题,Hash / Linked / Tree 三剑客各司其职
- Map 解决键值映射问题,从 HashMap 的无序高性能到 TreeMap 的有序慢速,再到 ConcurrentHashMap 的并发高性能,层次分明
- Queue/Deque 解决排队问题,ArrayDeque 已成为 Stack 和 LinkedList 队列的最佳替代
选择集合类时遵循三问原则:(1) 是否需要线程安全?(2) 是否关心元素顺序?(3) 读写频率如何?回答这三个问题,就能从框架全景图中精准定位最合适的实现。