Java 集合研究
允许存储 null
的集合
List 接口的实现类
- **
ArrayList
**:允许null
。 - **
LinkedList
**:允许null
。 - **
Vector
**:允许null
。 - **
Stack
**:允许null
(继承自Vector
)。
Set 接口的实现类
- **
HashSet
**:允许null
,但最多只能有一个null
。 - **
LinkedHashSet
**:允许null
,但最多只能有一个null
。 - **
TreeSet
**:允许null
,但前提是提供的比较器能处理null
。如果使用自然排序(Comparable
接口),则不允许null
。
Queue 接口的实现类
- **
LinkedList
**(同时是Queue
和List
的实现):允许null
。 PriorityQueue
:不允许null
。ArrayDeque
:不允许null
。
允许存储 null
的 Map
Map 接口的实现类
- **
HashMap
**:键和值都允许null
,键最多只能有一个null
。 - **
LinkedHashMap
**:键和值都允许null
,键最多只能有一个null
。 Hashtable
:不允许null
键或null
值。- **
TreeMap
**:键允许null
的条件同TreeSet
,即必须提供支持null
的比较器;值允许null
。
Java 并发包(java.util.concurrent
)的集合
ConcurrentHashMap
:不允许null
键或null
值。- **
CopyOnWriteArrayList
**:允许null
。 - **
CopyOnWriteArraySet
**:允许null
。 ConcurrentSkipListMap
:不允许null
键,但允许null
值。ConcurrentSkipListSet
:不允许null
。LinkedBlockingQueue
、ArrayBlockingQueue
、PriorityBlockingQueue
:不允许null
。DelayQueue
:不允许null
。SynchronousQueue
:不允许null
。LinkedTransferQueue
:不允许null
。
总结
- 允许
null
的集合主要是基于List
和Set
接口的实现类,如ArrayList
、LinkedList
、HashSet
、LinkedHashSet
等。 - 并发集合一般不允许
null
,例如ConcurrentHashMap
和大多数Queue
实现类。
推荐做法
尽量避免在集合中使用 null
,尤其是在并发场景中,以免增加代码复杂性和潜在错误风险。
Fail-Fast 与 Fail-Safe 机制
在 Java 的集合框架中,fail-fast
和 fail-safe
是两种应对集合在迭代期间结构修改(structural modification)时的机制。它们主要体现在 迭代器(Iterator) 的行为上。以下是两种机制的详细介绍:
Fail-Fast
Fail-Fast 是 Java 集合框架中的一种快速失败(Fail-Fast)机制。当多个线程或方法对同一集合对象进行修改时,如果在迭代过程中检测到集合结构被改变(如添加、删除、或修改元素),迭代器会抛出 **ConcurrentModificationException
**,从而防止程序继续运行可能导致不一致的结果。
Fail-Fast 的目标是尽早发现错误,避免隐藏的并发问题或逻辑漏洞。大多数 Java 集合类(如 ArrayList
、HashMap
、HashSet
等)都默认采用 fail-fast
机制。
结构性修改(Structural Modification):
- 结构性修改 是指改变集合的结构,例如添加或删除元素(但通过迭代器自身的
remove()
方法不会触发异常)。 - 例如,通过集合的
add()
、remove()
方法直接操作会被认为是结构性修改。
Fail-Fast 机制的实现原理
modCount
字段
大多数集合类(如ArrayList
、HashMap
)中维护了一个modCount
字段,用来记录集合结构的修改次数(结构性修改指影响集合的元素个数或顺序的操作,如add
、remove
)。迭代器的
expectedModCount
- 当调用集合的
iterator()
方法创建迭代器时,迭代器会保存集合当前的modCount
值到一个expectedModCount
变量中。 - 在迭代器的每次操作(如
next()
、remove()
)中,都会比较modCount
和expectedModCount
是否相等。
- 当调用集合的
检测到修改时抛异常
- 如果
modCount
和expectedModCount
不相等,表明集合在迭代期间被修改了,迭代器会抛出 **ConcurrentModificationException
**。
- 如果
Fail-Fast 示例代码
1 | import java.util.ArrayList; |
输出:
1 | A |
Fail-Fast 的特点
- 即时性:在集合结构被并发修改后,迭代器会尽快抛出异常,提醒开发者问题所在。
- 非安全性:Fail-Fast 机制无法保证迭代器在修改后继续工作,因此它是一个快速失败的检测机制,而非线程安全的解决方案。
Fail-Fast 的限制
单线程场景的限制
Fail-Fast 机制在单线程中同样会抛出异常。如果在迭代期间直接对集合进行修改,即使没有并发,也会触发异常。多线程场景中
- Fail-Fast 机制在多线程环境下是非确定的(非线程安全)。如果一个线程正在迭代集合,另一个线程修改了集合,则可能在某次检测到修改时抛出异常,但这种检测不是实时的。
Fast-Safe
什么是 Fail-Safe 机制?
Fail-Safe 是一种迭代机制,与 Java 的集合框架中的 Fail-Fast 相对。Fail-Safe 机制保证迭代器在集合被修改时不会抛出 **ConcurrentModificationException
**,并且能够继续正常运行。它的关键特性是操作的是集合的 副本 或基于弱一致性视图,而不是集合的原始数据。
Fail-Safe 机制的实现原理
Fail-Safe 的核心原理是 迭代器不直接操作集合的原始数据结构,而是基于集合的 副本 或提供了对弱一致性的支持。这种机制确保了以下两点:
- 即使在迭代期间集合被修改,副本或弱一致性视图不会受到影响。
- 修改操作不会影响正在进行的迭代过程,因此迭代器可以安全地继续运行。
Fail-Safe 机制的关键特点
- 线程安全:Fail-Safe 机制设计为多线程环境下使用,适用于高并发场景。
- 弱一致性(Weak Consistency):迭代器提供的集合视图是弱一致的,可能不是实时更新的。
- **不会抛出
ConcurrentModificationException
**:修改集合不会中断迭代过程。
Fail-Safe 示例
基于副本的实现(CopyOnWriteArrayList
)
CopyOnWriteArrayList
是 Java 并发包(java.util.concurrent
)中的一个线程安全的 List,它采用 写时复制(Copy-On-Write) 的策略,即:
- 每次对集合的修改(如
add
或remove
),都会创建一个新的副本并对其进行操作。 - 迭代器操作的是旧副本,因此不会受集合修改的影响。
1 | import java.util.concurrent.CopyOnWriteArrayList; |
输出:
1 | A |
分析:
- 在迭代期间添加元素,迭代器并不会感知到新元素,因此不抛异常。
- 修改后的集合会反映在下一次迭代操作或显式访问中。
弱一致性实现(ConcurrentHashMap
)
ConcurrentHashMap
是一种线程安全的哈希表。它通过分段锁或 CAS(Compare-And-Swap)操作保证了高效的并发访问,同时支持弱一致性:
- 迭代器不会抛出异常,但迭代的结果可能不包含迭代开始后添加的元素。
1 | import java.util.concurrent.ConcurrentHashMap; |
输出:
1 | 1 -> A |
分析:
- 迭代器无法感知到修改后的 “4=D” 键值对。
- 集合最终包含修改后的内容。
Fail-Safe 的优缺点
优点
- 线程安全:允许多个线程安全地访问和修改集合。
- 无中断性:不会因
ConcurrentModificationException
中断程序。 - 设计直观:对于高并发场景,避免了手动同步或复杂的锁机制。
缺点
- 性能开销:在基于副本的实现中(如
CopyOnWriteArrayList
),每次写操作都会创建新的副本,可能消耗较多内存,且写入操作效率较低。 - 弱一致性:迭代器的结果可能是过时的,尤其在修改频繁的场景下。
Fail-Safe 的适用场景
- 高并发场景:需要多个线程同时安全访问和修改集合,例如线程池管理或任务调度系统。
- 读多写少:如
CopyOnWriteArrayList
,适用于读操作频繁但写操作较少的场景。 - 允许弱一致性:对于修改实时性要求不高的应用,例如日志系统或统计系统。
Fail-Fast vs. Fail-Safe 对比总结
属性 | Fail-Fast | Fail-Safe |
---|---|---|
检测机制 | 使用 modCount 检测集合修改 |
使用集合的副本进行遍历 |
是否抛异常 | 是(ConcurrentModificationException ) |
否 |
性能 | 较高,因为直接操作集合 | 较低,因创建副本而消耗额外内存 |
支持的集合类型 | 非线程安全集合(如 ArrayList 、HashMap ) |
线程安全集合(如 CopyOnWriteArrayList ) |
修改时行为 | 抛异常以防止并发修改导致的不一致性 | 不抛异常,修改不影响遍历 |
使用场景:
Fail-Fast:
- 用于希望尽早发现错误的场景。
- 适合单线程操作,或者严格限制修改的环境。
Fail-Safe:
- 用于多线程环境,需要容忍遍历期间集合被修改。
- 适合读多写少的场景,例如配置数据或状态数据。