Java集合框架深度剖析:结构、并发与设计模式全解析
title: 2.集合框架
目录
1.集合框架概述与基本使用
学习并理解 Java 集合框架的整体结构及其各个常见集合类。
2.底层实现及原理
深入研究集合框架背后的底层实现。
3.线程安全与并发场景
了解在多线程环境中,如何保证集合的线程安全。
4.设计模式与源码设计思想
探讨集合框架中的设计模式(如工厂模式、单例模式等)及其在源码中的应用。
1.集合框架概述与基本使用
集合体系结构概览
Java 的集合框架(Collection Framework)是一个强大且统一的 API,用于存储和操作对象数据。它大致可以分为两大体系:
Collection 与 Map 的两大体系
Collection
接口是集合层次结构的根,用于表示一组对象(元素),其子接口包括:List
:有序集合,允许重复元素。Set
:不允许重复元素,无序或有序视实现而定。Queue
:支持队列操作的集合,通常用于按一定顺序处理元素(如 FIFO)。
Map
接口独立于Collection
,用于存储**键值对(key-value)**映射关系:- 键唯一,值可重复。
- 典型实现包括
HashMap
、TreeMap
、LinkedHashMap
等。
Collection 下的 List、Set、Queue 简介
接口 | 特点 | 常见实现类 |
---|---|---|
List | 有序,可重复 | ArrayList , LinkedList , Vector |
Set | 无序,不可重复 | HashSet , LinkedHashSet , TreeSet |
Queue | 先进先出(FIFO) | LinkedList , PriorityQueue , ArrayDeque |
• Map 接口的独立地位
Map
不继承Collection
,因为其语义(键值映射)与传统集合不同。- 其主要操作包括:添加 (
put
)、获取 (get
)、删除 (remove
)、遍历等。
接口 vs 实现类的关系图解
Iterable|Collection Map/ | \ |List Set Queue SortedMap| | | |ArrayList HashSet LinkedList TreeMap
核心接口与通用方法Java 集合框架中的所有类都遵循统一的接口规范,使得学习与使用变得更为一致和高效。
• add()、remove()、contains() 等基础方法
add(E e)
:向集合中添加元素。remove(Object o)
:移除指定对象(第一次出现)。contains(Object o)
:判断集合中是否包含该对象。isEmpty()
:判断集合是否为空。size()
:获取集合大小。
注意: 不同集合对
add()
的行为不同,如Set
添加重复元素会失败。
equals()、hashCode() 在集合中的作用
Set
、Map
等集合依赖equals()
和hashCode()
判断对象是否“相等”。- 若自定义对象作为集合元素或 Map 的键,必须重写这两个方法以避免逻辑错误。
@Override
public boolean equals(Object obj) {// 比较字段值
}
@Override
public int hashCode() {// 返回基于字段的 hash
}
泛型支持与类型安全
-
Java 5 引入泛型后,集合类支持类型参数:
List<String> list = new ArrayList<>();
-
避免类型转换错误(
ClassCastException
):String s = list.get(0); // 不再需要强制转换
fail-fast 与 fail-safe 行为差异
- fail-fast(快速失败):
- 集合在结构被修改(非通过迭代器)时,遍历会抛出
ConcurrentModificationException
。 - 如:
ArrayList
,HashMap
,HashSet
- 集合在结构被修改(非通过迭代器)时,遍历会抛出
- fail-safe(安全失败):
- 基于副本的遍历,不抛异常,但不保证实时性。
- 如:
CopyOnWriteArrayList
,ConcurrentHashMap
集合框架中的遍历方式
集合遍历是集合使用中最常见的操作之一,Java 提供了多种方式。
Iterator / enhanced for / forEach
-
Iterator
是最通用的遍历方式:Iterator<String> it = list.iterator(); while (it.hasNext()) {String s = it.next(); }
-
for-each
语法糖:for (String s : list) {// 自动调用 Iterator }
-
Collection.forEach(Consumer)
(Java 8+):list.forEach(s -> System.out.println(s));
ListIterator 的前后遍历
-
仅
List
接口支持ListIterator
,可双向遍历:ListIterator<String> it = list.listIterator(); while (it.hasNext()) {String s = it.next(); } while (it.hasPrevious()) {String s = it.previous(); }
-
可通过
add()
、remove()
等在遍历中安全修改集合。
lambda 与 stream 的遍历新姿势(Java 8+)
-
Java 8 引入 Stream API,使集合操作更函数式和声明式:
list.stream().filter(s -> s.startsWith("A")).map(String::toUpperCase).forEach(System.out::println);
-
并行流(parallelStream)可加速处理大数据量(注意线程安全)。
List 接口与实现类详解
List 是最常用的集合类型,特点是:有序、可重复。
ArrayList
(顺序数组)
- 底层结构:动态数组(Object[])
- 扩容机制:初始容量为10,每次扩容为原容量的 1.5 倍
- 优点:
- 随机访问性能高(
O(1)
) - 内存连续,缓存友好
- 随机访问性能高(
- 缺点:
- 插入/删除慢(涉及数组复制)
- 使用场景:
- 读多写少的顺序数据
- 进阶:
ensureCapacity()
可提前扩容避免重复复制- 可配合
Collections.synchronizedList
实现线程安全
LinkedList
(链表实现)
- 底层结构:双向链表
- 优点:
- 插入/删除效率高(不需位移)
- 缺点:
- 随机访问慢(
O(n)
)
- 随机访问慢(
- 支持队列操作:可作为
Deque
使用(支持栈/队列) - 使用场景:
- 数据频繁插入/删除
Vector
(线程安全的 ArrayList)
- 与
ArrayList
类似,但所有方法都被synchronized
修饰 - 几乎被淘汰,推荐使用
Collections.synchronizedList(new ArrayList<>())
Set 接口与实现类详解
Set 体现的是数学集合语义:无序、不可重复。
HashSet
(最常用)
- 底层结构:基于
HashMap
实现,元素作为键,值为常量PRESENT
- 添加逻辑:依赖
hashCode()
与equals()
保证唯一性 - JDK 8 优化:链表过长转为红黑树(链表长度 > 8 且桶大小 > 64)
- 特点:
- 查询/添加/删除时间复杂度为
O(1)
(理想情况)
- 查询/添加/删除时间复杂度为
- 常见误区:
- 忘记重写
equals/hashCode
导致“重复元素”失效
- 忘记重写
LinkedHashSet
- 底层结构:继承
HashSet
+ 双向链表 - 保持元素插入顺序
- 适合需要“去重 + 有序”场景
** TreeSet
**
- 底层结构:红黑树(
TreeMap
支撑) - 自动排序:元素需实现
Comparable
或使用自定义Comparator
- 时间复杂度:添加/删除/查询为
O(log n)
- 适合对数据进行自动排序、范围查询
Map 接口与实现类详解
Map 是 Java 中用于处理键值对映射的核心接口。
HashMap
(最常用)
- 底层结构:
- JDK 7:数组 + 链表
- JDK 8:数组 + 链表 + 红黑树
- 容量/负载因子:
- 初始容量默认 16,负载因子 0.75
- 扩容机制:
- 超过阈值(容量 × 负载因子)后,容量翻倍
- 冲突处理:
- 同 hash 值元素存储在链表或红黑树中(树化条件如上)
- 非线程安全
进阶:
- 键的要求:必须实现
hashCode()
与equals()
- JDK 8 之后性能显著提升(链表转红黑树)
- Hash 冲突多时性能可能降为 O(n),树化避免此问题
LinkedHashMap
- 底层结构:继承
HashMap
,同时维护插入顺序的双向链表 - 保持元素插入顺序(也可以按访问顺序)
- LRU 实现方式:
- 构造时指定
accessOrder=true
,并重写removeEldestEntry()
- 构造时指定
TreeMap
-
基于红黑树,键需排序(
Comparable
或Comparator
) -
适合范围查找、自动排序,如:
map.subMap(10, 20); // 获取 key 在 10~20 之间的子映射
Hashtable
(线程安全但已淘汰)
- 所有方法都加了
synchronized
,效率低 - 推荐使用
ConcurrentHashMap
Queue 接口与实现类详解
Queue 表现先进先出(FIFO)行为,广泛用于任务调度、缓存等。
LinkedList
实现 Queue
- 实现了
Queue
和Deque
- 支持头尾插入/删除(栈/队列双模式)
PriorityQueue
- 基于堆(最小堆),元素自动排序
- 默认使用
Comparable
,也可传入Comparator
- 常用于任务调度、优先队列
ArrayDeque
- 双端队列,高效替代
Stack
和LinkedList
- 无容量限制,头尾操作
O(1)
- 比
LinkedList
更轻量级,无链表指针开销
对比汇总
接口 | 实现类 | 底层结构 | 有序性 | 线程安全 | 特点 |
---|---|---|---|---|---|
List | ArrayList | 数组 | 有序 | × | 快速随机访问 |
List | LinkedList | 双向链表 | 有序 | × | 快速增删 |
Set | HashSet | Hash 表 | 无序 | × | 快速查重 |
Set | TreeSet | 红黑树 | 排序 | × | 自动排序 |
Map | HashMap | Hash 表 | 无序 | × | 快速 key-value 映射 |
Map | TreeMap | 红黑树 | 排序 | × | 按 key 排序 |
Queue | PriorityQueue | 堆 | 排序 | × | 优先处理 |
Queue | ArrayDeque | 循环数组 | 有序 | × | 双端操作 |
2.底层实现及原理
因为Java集合并没有太多理解困难的抽象知识,更多是在使用中的选择和应用,对于实现原理和源码分析已经有很多成熟的文章和视频,所以我不再重复赘述。
Java集合常见面试题总结(上) | JavaGuide
CarpenterLee/JCFInternals: 深入理解Java集合框架
google/guava: Google core libraries for Java
3.线程安全与并发场景
1.线程不安全集合的问题
在多线程并发环境中,如果使用 Java 中默认的集合类(如 ArrayList
、HashMap
、LinkedList
等),很容易出现数据不一致、程序异常甚至死循环等问题。这是因为这些集合类本身没有线程安全保障,多个线程同时读写时会引发竞态条件。
示例一:ArrayList 的并发问题
java复制编辑List<Integer> list = new ArrayList<>();Runnable task = () -> {for (int i = 0; i < 1000; i++) {list.add(i);}
};Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();System.out.println("Size: " + list.size());
预期结果:2000
实际可能结果:小于 2000,甚至抛出 ConcurrentModificationException
或 ArrayIndexOutOfBoundsException
。
原因:多个线程同时调用 add()
,在扩容或修改内部数组时发生冲突,导致数据错乱或覆盖。
示例二:HashMap 的并发死循环(JDK 1.7)
java复制编辑Map<Integer, Integer> map = new HashMap<>();Runnable task = () -> {for (int i = 0; i < 10000; i++) {map.put(i, i);}
};Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
在 JDK 1.7 中,如果在多线程环境下频繁 put
,可能会触发 rehash 时链表形成循环,导致 CPU 100% 卡死在遍历中(虽然 JDK 1.8 改进为红黑树后几率降低,但仍不安全)。
因此,在并发环境中使用这些集合类需要格外小心,不能直接使用,应使用线程安全版本或并发集合类替代。
2.线程安全的集合类
在多线程环境中,普通集合类(如 ArrayList
、HashMap
)由于缺乏内部同步机制,无法保障并发操作下的数据一致性,容易引发线程安全问题。为了解决这一问题,Java 提供了多种线程安全的集合实现方案,分别适用于不同的使用场景。
1. 使用 Collections.synchronizedXXX 包装
Java 的 Collections
工具类提供了若干静态方法,可以将原本非线程安全的集合包装成线程安全的版本,例如:
java复制编辑List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());
这些包装方法的底层实现通过 synchronized
关键字为集合的每个操作加锁,从而保证线程安全。但需要注意:
- 对集合的迭代仍需手动同步;
- 在高并发环境下性能较差,因为锁粒度粗,每次访问都串行化处理。
这种方式简单直接,适合轻量级的并发需求场景,但并不推荐用于复杂或高频操作的系统中。
2. Concurrent 包下的高性能集合类
为了提供更优的并发性能,Java 5 引入了 java.util.concurrent
包,内置了多种基于分段锁或无锁机制的线程安全集合,它们在企业开发中广泛应用:
ConcurrentHashMap
是HashMap
的线程安全替代品,JDK 7 使用分段锁(Segment),JDK 8 开始改为使用 CAS + synchronized 实现,具有更高的并发性能。适合高读写并发场景。CopyOnWriteArrayList
基于写时复制(Copy-On-Write)机制,每次修改(如添加、删除)时都会复制整个底层数组,写操作代价高,但读操作无需加锁,非常适合读多写少的场景。ConcurrentLinkedQueue
是一个基于非阻塞算法的无界线程安全队列,采用 CAS 操作保证并发安全,适用于高吞吐量的消息队列模型中。ConcurrentSkipListMap
/ConcurrentSkipListSet
基于跳表结构的线程安全有序集合,支持并发访问的同时保持元素有序,适用于需要排序或范围查询的场景。
这些集合的设计核心是降低锁粒度,避免全局锁,通过细粒度锁或乐观并发控制机制(如 CAS)来提升并发能力。
3. 并发集合的使用建议
- 读多写少:优先考虑
CopyOnWriteArrayList
。 - 高并发读写:使用
ConcurrentHashMap
。 - 高性能队列:采用
ConcurrentLinkedQueue
。 - 有序并发集合:考虑
ConcurrentSkipListMap
或ConcurrentSkipListSet
。
3.Concurrent 包下的并发集合原理
为了提升集合在并发场景下的性能和可扩展性,Java 的 java.util.concurrent
包引入了多种并发集合,其设计核心在于降低锁粒度、分离读写操作和使用无锁算法,相比传统的同步集合性能更优。
以下是几种常见并发集合的底层原理简介:
1. ConcurrentHashMap
ConcurrentHashMap
是最常用的线程安全 Map
实现:
- JDK 7:使用分段锁(Segment)
- 将整个 HashMap 拆分成多个 Segment,每个 Segment 维护一个独立的哈希表。
- 不同 Segment 之间互不干扰,提升并发访问性能。
- 缺点是默认只支持 16 个 Segment,扩展性有限。
- JDK 8:使用 CAS + synchronized
- 取消了 Segment,使用数组 + 链表/红黑树的结构。
- 写操作使用
synchronized
锁定单个桶,读操作通过volatile
保证可见性,避免加锁。 - 扩容时也采用分布式迁移,减少阻塞。
优点: 高性能、高可扩展性、适合读写并发场景。
注意: 并不支持 null 键或 null 值。
2. CopyOnWriteArrayList
适合读多写少的场景,其核心思想是写时复制:
- 每次修改操作(如
add()
、remove()
)都会复制一份新数组。 - 读操作无需加锁,因为读的是旧数组副本,天然线程安全。
- 写操作通过
ReentrantLock
保证串行化。
优点: 读操作无锁,读性能极高。
缺点: 写操作开销大,不适合频繁修改的场景。
3. ConcurrentLinkedQueue
是一个基于 非阻塞链表 的并发队列,实现了 无锁队列算法:
- 使用 CAS(Compare-And-Swap) 操作保证原子性,避免传统加锁开销。
- 基于 Michael-Scott 非阻塞队列算法实现,入队和出队操作互不干扰。
优点: 高吞吐,适合生产者-消费者模型、任务调度等场景。
缺点: 在极高并发下可能存在 ABA 问题(JDK 已优化)。
4. ConcurrentSkipListMap / Set
使用跳表(Skip List)实现的有序并发集合:
- 支持并发访问的同时保持元素的自然顺序或指定顺序。
- 跳表是一种多层级链表,平均查找、插入、删除复杂度为 O(log n)。
- 内部采用 CAS 保证节点插入删除的原子性,同时使用
ReentrantLock
做局部加锁。
优点: 支持范围查询、有序遍历,是线程安全的 TreeMap 替代品。
Java 并发集合的底层实现都体现了对传统加锁方式的优化,通过合理的算法设计和内存模型控制,使得集合在多线程环境下既安全又高效。掌握它们的原理,对于设计线程安全的数据结构和排查并发 Bug 都非常重要。
4.使用建议与场景
在多线程环境下,选择正确的集合类至关重要。使用不当会导致数据竞争、性能瓶颈甚至严重的线程安全问题。以下是一些实用建议和典型场景,帮助开发者做出合理选择。
1. 明确读写比重
- 读多写少:
- 使用
CopyOnWriteArrayList
或CopyOnWriteArraySet
。 - 避免加锁带来的读阻塞,适合缓存、配置列表等。
- 使用
- 读写并发:
- 使用
ConcurrentHashMap
、ConcurrentSkipListMap
等。 - 它们对读写进行了优化,适合共享数据频繁变动的业务逻辑。
- 使用
- 写密集场景:
- 避免使用写时复制类(如 CopyOnWrite 系列)。
- 可以使用
ConcurrentLinkedQueue
、BlockingQueue
等支持高并发写入的结构。
2. 是否需要顺序保证
- 若需要 有序遍历 或 范围查询,推荐使用:
ConcurrentSkipListMap
(有序 Map)ConcurrentSkipListSet
(有序 Set)
- 若对顺序不敏感:
ConcurrentHashMap
更加高效,占用更少内存。
3. 是否涉及阻塞行为
- 若集合用于生产者-消费者模型,考虑使用:
LinkedBlockingQueue
ArrayBlockingQueue
DelayQueue
、PriorityBlockingQueue
等
- 这些阻塞队列内置
take()
和put()
方法,可以自然地控制线程等待和唤醒,避免手动加锁。
4. 不要误用线程不安全集合
在并发场景中避免使用:
ArrayList
HashMap
LinkedList
HashSet
等
这些集合在并发读写中没有任何线程安全机制,容易导致数据不一致、死循环(如旧版 HashMap
扩容)或程序崩溃。
5. 警惕性能陷阱
Collections.synchronizedXXX()
虽然提供了线程安全包装,但性能差,锁粒度粗,容易成为瓶颈。Hashtable
是遗留类,已被ConcurrentHashMap
取代,避免使用。
场景描述 | 推荐集合 |
---|---|
读多写少 | CopyOnWriteArrayList |
高并发读写 | ConcurrentHashMap |
有序并发访问 | ConcurrentSkipListMap / Set |
阻塞队列 / 消费者模型 | LinkedBlockingQueue, ArrayBlockingQueue |
简单线程安全包装(低性能) | Collections.synchronizedList 等 |
4.设计模式与源码设计思想
这部分还会更新扩充
Java集合框架是基于许多经典设计模式和原则来实现的,它通过不同的集合类和接口,为开发者提供了高效、灵活的数据存储解决方案。在深入分析Java集合框架时,我们可以发现其中有大量设计模式的应用以及源码设计思想的体现。
一、集合框架中的常见设计模式
-
工厂模式(Factory Pattern)
- 概述:工厂模式用于创建对象的接口,但不暴露对象创建的具体实现。
- 在集合框架中的应用:
Collections
类和List
、Set
、Map
等接口的实现都遵循了工厂模式。例如,Collections
类提供了静态工厂方法(如singletonList()
、emptyList()
等)来创建不可变集合对象。 - 源码示例:
public static <T> List<T> singletonList(T o) {return new SingletonList<>(o); }
这里的
singletonList
就是工厂方法,通过封装集合的创建逻辑,提供特定类型的集合实例。 -
单例模式(Singleton Pattern)
- 概述:确保某个类只有一个实例,并提供全局访问点。
- 在集合框架中的应用:
EnumSet
是一个典型的单例模式应用。因为EnumSet
只能处理枚举类型,所以它通过内部静态方法确保了集合的唯一性和单例模式。 - 源码示例:
public static <E extends Enum<E>> EnumSet<E> of(E first, E... rest) {return EnumSet.allOf(first.getDeclaringClass()).clone(); }
EnumSet
确保了枚举集合的单一性,不会创建多余的实例。 -
策略模式(Strategy Pattern)
- 概述:通过封装算法,允许在运行时切换算法。
- 在集合框架中的应用:在
TreeSet
和TreeMap
中,元素的比较方式由Comparator
接口策略化。Comparator
可以在运行时被替换,从而改变元素的排序方式。 - 源码示例:
public TreeSet(Comparator<? super E> comparator) {this.comparator = comparator; }
TreeSet
和TreeMap
允许用户通过传入不同的Comparator
来决定集合的排序策略。 -
装饰者模式(Decorator Pattern)
- 概述:允许动态地给对象添加额外的功能。
- 在集合框架中的应用:
List
、Set
和Map
等集合接口有多个装饰类,如Collections.unmodifiableList()
,它封装了一个集合对象,为集合提供只读功能。 - 源码示例:
public static <T> List<T> unmodifiableList(List<? extends T> list) {return new UnmodifiableList<>(list); }
UnmodifiableList
是对原集合的装饰,使得外部无法修改该集合。 -
代理模式(Proxy Pattern)
- 概述:通过代理对象控制对原对象的访问。
- 在集合框架中的应用:
CopyOnWriteArrayList
类使用了代理模式,它的行为被“复制”到一个新的副本中,以避免直接修改原集合时的并发问题。 - 源码示例:
public boolean add(E e) {final ReentrantLock lock = this.lock;lock.lock();try {Object[] elements = getArray();int length = elements.length;// Create a new array for every modification.Object[] newElements = Arrays.copyOf(elements, length + 1);newElements[length] = e;setArray(newElements);return true;} finally {lock.unlock();} }
这个设计通过复制原数组来避免在并发修改集合时出现问题,从而使得集合在多线程环境下保持线程安全。
二、Java集合中的源码设计思想
- 高内聚低耦合
- 概述:集合类的设计遵循了高内聚低耦合的原则,意味着每个集合类仅负责实现集合的特定功能,而与其他类或组件的耦合度尽量保持在最低。
- 体现:通过接口和抽象类,Java集合框架使得不同类型的集合可以灵活替换,并且实现了面向接口编程。例如,
List
、Set
、Map
都是集合接口,具体的实现类如ArrayList
、HashSet
、HashMap
等都有各自独立的实现。
- 接口与抽象类的运用
- 概述:集合框架的设计采用了大量的接口和抽象类来解耦集合的实现与使用。
- 体现:通过
Collection
、List
、Set
、Map
等接口,集合框架能够灵活地支持不同类型的集合。在实现细节方面,诸如AbstractList
和AbstractSet
提供了基础的功能实现,让具体的实现类可以继承并加以拓展。
- 通用性与灵活性
- 概述:Java集合框架强调通用性,几乎所有集合类都遵循了一种标准的接口规范,确保了不同集合类之间的通用操作。
- 体现:通过一组统一的接口和方法,所有集合类(如
add()
、remove()
、size()
)都遵循相同的约定,这使得开发者可以更轻松地切换不同类型的集合(如ArrayList
与LinkedList
)而不改变代码逻辑。
- 延迟初始化与懒加载
- 概述:在某些集合类中,采用延迟初始化和懒加载的策略来优化性能。
- 体现:例如,在
HashMap
中,内部数组并不是在初始化时就创建,而是在需要时才进行扩容,确保内存的有效利用。Lazy
和synchronized
在某些线程安全的集合类(如CopyOnWriteArrayList
)中发挥作用,减少不必要的计算和开销。
Java集合框架不仅仅提供了多种高效的数据结构和操作方法,它背后也体现了许多经典的设计模式和良好的源码设计思想。这些模式和思想的应用使得集合框架在不同的场景下都能提供灵活、高效、可扩展的解决方案。深入理解这些设计模式和源码实现,能够帮助开发者在使用集合框架时更加得心应手,同时也能够在设计自己的系统时,借鉴这些模式来提升代码的可维护性、可扩展性和性能。