Java集合概述
Java 集合框架是 Java 标准库中最常用的基础设施之一。它提供了一组接口、实现类和工具方法,用于存储、查找、排序、去重、排队、映射关系和批量处理数据。
学习集合不能只记住“ArrayList 查询快、LinkedList 增删快、HashMap 查找快”这类结论。真正有价值的是理解:
- 集合接口之间的职责边界。
- 不同实现类的底层数据结构。
- 时间复杂度背后的原因。
- 扩容、哈希冲突、红黑树、迭代器、fail-fast 等机制。
- 开发中如何选型,如何避免常见坑。
# 1. 集合框架解决什么问题
数组长度固定、API 较少,适合结构简单、长度稳定的场景。业务开发中更多时候需要动态数据结构:
用户列表 -> List
唯一用户 ID -> Set
用户 ID 到用户 -> Map
待处理任务 -> Queue
最近访问记录 -> LinkedHashMap
优先级任务 -> PriorityQueue
集合框架将常见数据结构封装成统一 API,使业务代码不需要从零实现数组扩容、链表操作、哈希表、树结构和排序算法。
# 2. 集合接口体系
Java 集合分为两条主线:
Collection:单个元素的集合。Map:键值对映射,不继承Collection。
Iterable
└─ Collection
├─ List
│ ├─ ArrayList
│ └─ LinkedList
├─ Set
│ ├─ HashSet
│ ├─ LinkedHashSet
│ └─ TreeSet
└─ Queue
├─ Deque
│ ├─ ArrayDeque
│ └─ LinkedList
└─ PriorityQueue
Map
├─ HashMap
├─ LinkedHashMap
├─ TreeMap
├─ Hashtable
└─ ConcurrentHashMap
接口职责:
| 接口 | 数据语义 | 是否允许重复 | 是否有序 | 典型实现 |
|---|---|---|---|---|
List | 线性表 | 允许 | 按索引有序 | ArrayList、LinkedList |
Set | 不重复集合 | 不允许 | 取决于实现 | HashSet、TreeSet |
Queue | 队列 | 通常允许 | 按出队规则 | LinkedList、PriorityQueue |
Deque | 双端队列 | 通常允许 | 两端操作 | ArrayDeque、LinkedList |
Map | 键值映射 | key 不重复 | 取决于实现 | HashMap、TreeMap |
# 3. Iterable 与 Iterator
Iterable 让集合支持增强 for 循环。
for (String name : names) {
System.out.println(name);
}
编译后大致等价于:
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
String name = iterator.next();
System.out.println(name);
}
迭代模型:
Collection
│ iterator()
▼
Iterator
│ hasNext / next / remove
▼
逐个访问元素
集合遍历时删除元素,应优先使用迭代器的 remove(),不要直接调用集合的 remove()。
# 4. List
List 表示有序、可重复、可按索引访问的集合。
List<String> names = new ArrayList<>();
names.add("Tom");
names.add("Tom");
System.out.println(names.get(0));
常见实现对比:
| 实现 | 底层结构 | 随机访问 | 头尾操作 | 中间插入删除 | 内存占用 |
|---|---|---|---|---|---|
ArrayList | 动态数组 | 快,O(1) | 尾部快 | 需要移动元素 | 较低 |
LinkedList | 双向链表 | 慢,O(n) | 头尾快 | 找到节点后快 | 较高 |
实际业务中,ArrayList 往往是默认首选。很多人以为 LinkedList 插入删除一定更快,但如果插入删除前还要按索引定位节点,整体仍然是 O(n),而且链表节点对象带来额外内存和缓存不友好问题。
# 5. Set
Set 表示不重复集合。
Set<Long> userIds = new HashSet<>();
userIds.add(1L);
userIds.add(1L);
System.out.println(userIds.size()); // 1
常见实现:
| 实现 | 底层结构 | 顺序 | 适合场景 |
|---|---|---|---|
HashSet | HashMap | 不保证顺序 | 快速去重、快速判断存在 |
LinkedHashSet | LinkedHashMap | 插入顺序 | 去重并保留插入顺序 |
TreeSet | 红黑树 | 排序 | 去重并排序、范围查询 |
Set 判断重复依赖元素的相等语义。对于 HashSet,重点是 equals 和 hashCode;对于 TreeSet,重点是 compareTo 或 Comparator。
# 6. Map
Map 用于保存 key 到 value 的映射。
Map<Long, User> userMap = new HashMap<>();
userMap.put(1L, new User("Tom"));
User user = userMap.get(1L);
常见实现:
| 实现 | 底层结构 | key 顺序 | 典型场景 |
|---|---|---|---|
HashMap | 数组 + 链表/红黑树 | 不保证 | 默认映射结构 |
LinkedHashMap | HashMap + 双向链表 | 插入或访问顺序 | 有序遍历、LRU |
TreeMap | 红黑树 | key 排序 | 排序、范围查询 |
ConcurrentHashMap | 并发哈希表 | 不保证 | 并发读写 |
Map 的常用方法:
| 方法 | 作用 |
|---|---|
put | 添加或覆盖 key 对应 value |
get | 根据 key 获取 value |
remove | 删除 key |
containsKey | 判断 key 是否存在 |
computeIfAbsent | key 不存在时计算并放入 |
putIfAbsent | key 不存在时放入 |
merge | 合并旧值和新值 |
get(key) == null 不能直接说明 key 不存在,因为 value 也可能就是 null。需要区分时使用 containsKey。
# 7. Queue 与 Deque
Queue 表示队列,常见语义是先进先出。
入队 offer
│
▼
[A, B, C]
│
▼
出队 poll -> A
Deque 是双端队列,两端都能进出:
addFirst addLast
│ │
▼ ▼
[ head ... tail ]
▲ ▲
pollFirst pollLast
栈场景优先使用 ArrayDeque,不要再使用老的 Stack 类。
# 8. 时间复杂度总览
| 操作 | ArrayList | LinkedList | HashMap | TreeMap | HashSet |
|---|---|---|---|---|---|
| 随机访问 | O(1) | O(n) | 不适用 | 不适用 | 不适用 |
| 尾部添加 | 均摊 O(1) | O(1) | 不适用 | 不适用 | 不适用 |
| 中间插入 | O(n) | O(n) 定位 + O(1) 插入 | 不适用 | 不适用 | 不适用 |
| 查找元素 | O(n) | O(n) | 平均 O(1) | O(log n) | 平均 O(1) |
| 删除元素 | O(n) | O(n) | 平均 O(1) | O(log n) | 平均 O(1) |
| 有序遍历 | 按插入顺序 | 按链表顺序 | 不保证 | key 排序 | 不保证 |
复杂度是平均模型,不是绝对承诺。哈希冲突、扩容、比较器成本、对象分布、CPU 缓存命中都会影响真实性能。
# 9. fail-fast 与 fail-safe
很多集合迭代器是 fail-fast 的。遍历过程中如果集合被非迭代器方式结构性修改,可能抛出 ConcurrentModificationException。
for (String name : names) {
if (name.startsWith("A")) {
names.remove(name); // 可能抛 ConcurrentModificationException
}
}
正确方式:
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
String name = iterator.next();
if (name.startsWith("A")) {
iterator.remove();
}
}
机制:
集合结构修改 -> modCount + 1
迭代器创建时记录 expectedModCount
迭代时比较两者
不同 -> ConcurrentModificationException
fail-fast 是尽早发现错误的机制,不是线程安全保证。
# 10. 线程安全
大多数普通集合都不是线程安全的:
ArrayListLinkedListHashMapHashSetTreeMap
多线程读写时选择:
| 场景 | 推荐 |
|---|---|
| 高并发 Map | ConcurrentHashMap |
| 读多写少 List | CopyOnWriteArrayList |
| 阻塞队列 | ArrayBlockingQueue、LinkedBlockingQueue |
| 简单同步包装 | Collections.synchronizedList |
| 不可变数据共享 | List.of、Map.of 或不可变副本 |
同步包装集合在遍历时仍需要外部同步:
List<String> list = Collections.synchronizedList(new ArrayList<>());
synchronized (list) {
for (String value : list) {
System.out.println(value);
}
}
# 11. 不可变集合与只读视图
Collections.unmodifiableList(list) 返回只读视图,不是不可变副本。
List<String> source = new ArrayList<>();
List<String> view = Collections.unmodifiableList(source);
source.add("A");
System.out.println(view); // [A]
view 自己不能修改,但底层 source 变了,视图也会变。
如果需要不可变副本:
List<String> copy = List.copyOf(source);
或者:
List<String> list = List.of("A", "B");
注意:List.of、Set.of、Map.of 不允许 null。
# 12. 集合选型速查
| 需求 | 推荐 |
|---|---|
| 有序可重复列表 | ArrayList |
| 频繁头尾入队出队 | ArrayDeque |
| 快速去重 | HashSet |
| 去重并保持插入顺序 | LinkedHashSet |
| 去重并排序 | TreeSet |
| key-value 查找 | HashMap |
| key-value 且保持插入顺序 | LinkedHashMap |
| key-value 且按 key 排序 | TreeMap |
| 并发 key-value | ConcurrentHashMap |
| 生产消费队列 | BlockingQueue |
默认经验:
- List 默认选
ArrayList。 - 栈和队列默认选
ArrayDeque。 - Map 默认选
HashMap。 - 需要顺序时再换
LinkedHashMap或TreeMap。 - 并发场景不要靠普通集合硬撑。
# Tips 快问快答
Q:Collection 和 Collections 有什么区别?
A:Collection 是集合根接口,Collections 是集合工具类。
Q:Map 是 Collection 的子接口吗?
A:不是。Map 是键值对结构,与 Collection 平行。
Q:List、Set、Map 怎么快速区分? A:List 管顺序和重复,Set 管唯一性,Map 管 key 到 value 的映射。
Q:为什么集合里不能放基本类型?
A:泛型类型参数必须是引用类型,所以用 Integer、Long 等包装类。
Q:遍历时删除元素为什么会报错? A:普通集合迭代器检测到结构性修改后会 fail-fast。
Q:fail-fast 能保证并发安全吗? A:不能。它只是错误检测机制,不是同步机制。
Q:List.of 创建的集合能修改吗?
A:不能,它返回不可变集合,并且不允许 null。
Q:Collections.unmodifiableList 是不可变集合吗?
A:严格说不是,它是只读视图,底层集合变化时视图也会变化。
Q:集合选型最重要看什么? A:看是否需要顺序、是否允许重复、是否按 key 查找、是否并发、是否需要排序。
Q:为什么很多场景默认用 ArrayList?
A:它结构简单、随机访问快、内存局部性好,绝大多数业务列表都适合。