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**(同时是 QueueList 的实现):允许 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
  • LinkedBlockingQueueArrayBlockingQueuePriorityBlockingQueue不允许 null
  • DelayQueue不允许 null
  • SynchronousQueue不允许 null
  • LinkedTransferQueue不允许 null

总结

  1. 允许 null 的集合主要是基于 ListSet 接口的实现类,如 ArrayListLinkedListHashSetLinkedHashSet 等。
  2. 并发集合一般不允许 null,例如 ConcurrentHashMap 和大多数 Queue 实现类。

推荐做法

尽量避免在集合中使用 null,尤其是在并发场景中,以免增加代码复杂性和潜在错误风险。

Fail-Fast 与 Fail-Safe 机制

在 Java 的集合框架中,fail-fastfail-safe 是两种应对集合在迭代期间结构修改(structural modification)时的机制。它们主要体现在 迭代器(Iterator) 的行为上。以下是两种机制的详细介绍:


Fail-Fast

Fail-Fast 是 Java 集合框架中的一种快速失败(Fail-Fast)机制。当多个线程或方法对同一集合对象进行修改时,如果在迭代过程中检测到集合结构被改变(如添加、删除、或修改元素),迭代器会抛出 **ConcurrentModificationException**,从而防止程序继续运行可能导致不一致的结果。

Fail-Fast 的目标是尽早发现错误,避免隐藏的并发问题或逻辑漏洞。大多数 Java 集合类(如 ArrayListHashMapHashSet 等)都默认采用 fail-fast 机制。

结构性修改(Structural Modification):

  • 结构性修改 是指改变集合的结构,例如添加或删除元素(但通过迭代器自身的 remove() 方法不会触发异常)。
  • 例如,通过集合的 add()remove() 方法直接操作会被认为是结构性修改。

Fail-Fast 机制的实现原理

  1. modCount 字段
    大多数集合类(如 ArrayListHashMap)中维护了一个 modCount 字段,用来记录集合结构的修改次数(结构性修改指影响集合的元素个数或顺序的操作,如 addremove)。

  2. 迭代器的 expectedModCount

    • 当调用集合的 iterator() 方法创建迭代器时,迭代器会保存集合当前的 modCount 值到一个 expectedModCount 变量中。
    • 在迭代器的每次操作(如 next()remove())中,都会比较 modCountexpectedModCount 是否相等。
  3. 检测到修改时抛异常

    • 如果 modCountexpectedModCount 不相等,表明集合在迭代期间被修改了,迭代器会抛出 **ConcurrentModificationException**。

Fail-Fast 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class FailFastExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

Iterator<String> iterator = list.iterator();

while (iterator.hasNext()) {
System.out.println(iterator.next());
// 修改集合结构
list.add("D"); // 触发 Fail-Fast
}
}
}

输出:

1
2
A
Exception in thread "main" java.util.ConcurrentModificationException

Fail-Fast 的特点

  1. 即时性:在集合结构被并发修改后,迭代器会尽快抛出异常,提醒开发者问题所在。
  2. 非安全性:Fail-Fast 机制无法保证迭代器在修改后继续工作,因此它是一个快速失败的检测机制,而非线程安全的解决方案。

Fail-Fast 的限制

  1. 单线程场景的限制
    Fail-Fast 机制在单线程中同样会抛出异常。如果在迭代期间直接对集合进行修改,即使没有并发,也会触发异常。

  2. 多线程场景中

    • Fail-Fast 机制在多线程环境下是非确定的(非线程安全)。如果一个线程正在迭代集合,另一个线程修改了集合,则可能在某次检测到修改时抛出异常,但这种检测不是实时的。

Fast-Safe

什么是 Fail-Safe 机制?

Fail-Safe 是一种迭代机制,与 Java 的集合框架中的 Fail-Fast 相对。Fail-Safe 机制保证迭代器在集合被修改时不会抛出 **ConcurrentModificationException**,并且能够继续正常运行。它的关键特性是操作的是集合的 副本 或基于弱一致性视图,而不是集合的原始数据。


Fail-Safe 机制的实现原理

Fail-Safe 的核心原理是 迭代器不直接操作集合的原始数据结构,而是基于集合的 副本 或提供了对弱一致性的支持。这种机制确保了以下两点:

  1. 即使在迭代期间集合被修改,副本或弱一致性视图不会受到影响。
  2. 修改操作不会影响正在进行的迭代过程,因此迭代器可以安全地继续运行。

Fail-Safe 机制的关键特点

  1. 线程安全:Fail-Safe 机制设计为多线程环境下使用,适用于高并发场景。
  2. 弱一致性(Weak Consistency):迭代器提供的集合视图是弱一致的,可能不是实时更新的。
  3. **不会抛出 ConcurrentModificationException**:修改集合不会中断迭代过程。

Fail-Safe 示例

基于副本的实现(CopyOnWriteArrayList

CopyOnWriteArrayList 是 Java 并发包(java.util.concurrent)中的一个线程安全的 List,它采用 写时复制(Copy-On-Write) 的策略,即:

  • 每次对集合的修改(如 addremove),都会创建一个新的副本并对其进行操作。
  • 迭代器操作的是旧副本,因此不会受集合修改的影响。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.concurrent.CopyOnWriteArrayList;

public class FailSafeExample {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
list.add("C");

for (String item : list) {
System.out.println(item);
list.add("D"); // 不会抛出异常
}

System.out.println("最终集合:" + list);
}
}

输出:

1
2
3
4
A
B
C
最终集合:[A, B, C, D, D, D]

分析:

  • 在迭代期间添加元素,迭代器并不会感知到新元素,因此不抛异常。
  • 修改后的集合会反映在下一次迭代操作或显式访问中。

弱一致性实现(ConcurrentHashMap

ConcurrentHashMap 是一种线程安全的哈希表。它通过分段锁或 CAS(Compare-And-Swap)操作保证了高效的并发访问,同时支持弱一致性:

  • 迭代器不会抛出异常,但迭代的结果可能不包含迭代开始后添加的元素。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.concurrent.ConcurrentHashMap;

public class FailSafeExampleMap {
public static void main(String[] args) {
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put("1", "A");
map.put("2", "B");
map.put("3", "C");

for (String key : map.keySet()) {
System.out.println(key + " -> " + map.get(key));
map.put("4", "D"); // 不会抛出异常
}

System.out.println("最终集合:" + map);
}
}

输出:

1
2
3
4
1 -> A
2 -> B
3 -> C
最终集合:{1=A, 2=B, 3=C, 4=D}

分析:

  • 迭代器无法感知到修改后的 “4=D” 键值对。
  • 集合最终包含修改后的内容。

Fail-Safe 的优缺点

优点
  1. 线程安全:允许多个线程安全地访问和修改集合。
  2. 无中断性:不会因 ConcurrentModificationException 中断程序。
  3. 设计直观:对于高并发场景,避免了手动同步或复杂的锁机制。
缺点
  1. 性能开销:在基于副本的实现中(如 CopyOnWriteArrayList),每次写操作都会创建新的副本,可能消耗较多内存,且写入操作效率较低。
  2. 弱一致性:迭代器的结果可能是过时的,尤其在修改频繁的场景下。

Fail-Safe 的适用场景

  1. 高并发场景:需要多个线程同时安全访问和修改集合,例如线程池管理或任务调度系统。
  2. 读多写少:如 CopyOnWriteArrayList,适用于读操作频繁但写操作较少的场景。
  3. 允许弱一致性:对于修改实时性要求不高的应用,例如日志系统或统计系统。

Fail-Fast vs. Fail-Safe 对比总结

属性 Fail-Fast Fail-Safe
检测机制 使用 modCount 检测集合修改 使用集合的副本进行遍历
是否抛异常 是(ConcurrentModificationException
性能 较高,因为直接操作集合 较低,因创建副本而消耗额外内存
支持的集合类型 非线程安全集合(如 ArrayListHashMap 线程安全集合(如 CopyOnWriteArrayList
修改时行为 抛异常以防止并发修改导致的不一致性 不抛异常,修改不影响遍历

使用场景:

  1. Fail-Fast

    • 用于希望尽早发现错误的场景。
    • 适合单线程操作,或者严格限制修改的环境。
  2. Fail-Safe

    • 用于多线程环境,需要容忍遍历期间集合被修改。
    • 适合读多写少的场景,例如配置数据或状态数据。