前言
在实际开发过程中,我们经常会遇到遍历集合类的情况。但是其内部实现又是如何的呢?使用时需要注意的地方呢?本文一一道来。
常用遍历方式
Java集合类包含了List接口、Map接口和Set接口,分别看一下三者的便利方式。
- List接口的遍历方式
for (int i = 0; i < list.size(); i++) {
//doSomething
}
Iterator<String> it = list.iterator();
while (it.hasNext()) {
//doSomething
}
for (String s2 : list) {
//doSomething
}
- Map接口的遍历方式
for (Integer in : map.keySet()) {
//doSomething
}
Iterator<Map.Entry<Integer, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Integer, String> entry = it.next();
//doSomething
}
for (Map.Entry<Integer, String> entry : map.entrySet()) {
//doSomething
}
- Set接口的遍历方式
Iterator<string> it = set.iterator();
while (it.hasNext()) {
String value = it.next();
//doSomething
}
for(String s: set){
//doSomething
}
总结一下,集合类的遍历方式包括迭代器和for循环。
ConcurrentModificationException
在对集合迭代的过程中对集合进行一些修改的操作, 比如说add,remove之类的操作, 搞不好就会抛出ConcurrentModificationException, 这一点在API文档上也有说的。在迭代时只可以用迭代器进行删除。
我们以ArrayList为例,当我们使用迭代器遍历的时候,会频繁调用hasNext和next方法,其代码如下:
public boolean hasNext() {
return cursor != size;
}
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
而next方法调用了checkForComodification方法,看一下内部实现:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
继续跟踪modCount和expectedModCount两个变量,在Itr类的成员变量里对expectedModCount初始化的赋值:
int expectedModCount = modCount;
而modCount是AbstractList中的一个protected的变量, 在对集合增删的操作中均对modCount做了修改, 记录的是操作次数。通过判断遍历操作前后modCount的值是否相等,继而判断是否抛出异常。
查看ArrayList源码可以看到add和remove方法都会修改modCount的值,HashMap的也含有一个名为modCount的内部变量,处理逻辑跟ArrayList类似,不再赘述。因此不合适的使用上述方法均可能产生ConcurrentModificationException。
fail-fast和fast-safe
fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
在fail-safe机制下,任何对集合结构的修改都会在一个复制的集合上进行修改,因此不会抛出ConcurrentModificationException。但是fail-safe机制也有如下两个问题:1、需要复制集合,产生大量的无效对象,开销大;2、无法保证读取的数据是目前原始数据结构中的数据。
fail-fast解决办法
- 方案一:在遍历过程中所有涉及到改变modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。
- 方案二:使用CopyOnWriteArrayList来替换ArrayList。
CopyOnWriteArrayList是ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。 该类产生的开销比较大,但是在两种情况下,它非常适合使用:
- 1,在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时;
- 2:当遍历操作的数量大大超过可变操作的数量时;
遇到这两种情况使用CopyOnWriteArrayList来替代ArrayList再适合不过了。
总结
本文以常用集合类的遍历方式为起始,讲述了通过迭代器遍历可能引发的问题,并引出了内部的fail-fast机制,最后给出此机制的解决办法,希望能对读者有所启发。