在 Java 中,Set 和 List 可以循环遍历,因为其拥有可迭代能力,主要还是 Iterator 的功劳。
初识
首先看一下二者如何定义:
- fail-fast 一旦发现遍历的同时有其他人修改,则立即抛出异常
- fail-safe 当发现在遍历时有其他人修改,就牺牲一致性遍历整个集合
fail-fast
ArrayList 采用的是 fail-fast 机制。我们在遍历过程中,并不能添加或删除元素。
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(5);
for(Integer i : list){
System.out.println(i);
if(i == 3){
list.add(4);
}
}
System.out.println(list);
}
当我们在遍历 list 时,添加元素,就会得到异常输出。
1
2
3
Exception in thread “main” java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
at java.util.ArrayList$Itr.next(ArrayList.java:861)
2
3
Exception in thread “main” java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
at java.util.ArrayList$Itr.next(ArrayList.java:861)
fail-safe
在JDK中,还提供了一个CopyOnWriteArrayList,它就属于 fail-safe.
public static void main(String[] args) {
List<Integer> list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(5);
for(Integer i : list){
System.out.println(i);
if(i == 3){
list.add(4);
}
}
System.out.println(list);
}
仅仅将 ArrayList 变为了 CopyOnWriteArrayList,其他代码未改动。
结果能输出,但是丢失了一致性,我们在遍历过程中添加了元素,但是遍历过程中,并未得到该元素。最后一行,我打印了 List 遍历完后的元素,发现 4 已结被添加到列表中了,但是 for 循环并不能感知到该元素。
1
2
3
5
[1, 2, 3, 5, 4]
2
3
5
[1, 2, 3, 5, 4]
原因分析
在遍历 List 时,Java 会使用到迭代器 Iterator,而不同的List 实现,对应的迭代器实现不同。
ArrayList
在 ArrayList 实现中,部分关键实现代码如下:
public class ArrayList<E> {
private class Itr implements Iterator<E> {
int expectedModCount = modCount;
public E next() {
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
}
这里面主要有两个变量很关键:
- expectedModCount 是 list 原始的元素个数
- modCount 是每次遍历时,迭代器发现的元素个数
正常情况下,二者是相等的。但是要在遍历过程中调用 add 或者 remove 等修改元素个数的方法,就会使得 modCount 增加或减少,与 expectedModCount 不相等,从而抛出异常 ConcurrentModificationException。
CopyOnWriteArrayList
在 CopyOnWriteArrayList 实现中,部分关键实现代码如下:
public class CopyOnWriteArrayList<E> {
static final class COWIterator<E> implements ListIterator<E> {
private final Object[] snapshot;
private COWIterator(Object[] elements, int initialCursor) {
snapshot = elements;
}
public E next() {
return (E) snapshot[cursor++];
}
}
}
实现思路是通过创建一个临时数组,然后遍历这个临时数组 snapshot 来处理可能出现元素添加的情况。
这也就解释了,在 List 中添加元素,不能在控制台输入添加的元素。因为遍历的是原始 List 的快照,无论在循环中如何修改 List ,都不会受影响。