前言

在实际开发过程中,我们经常会遇到遍历集合类的情况。但是其内部实现又是如何的呢?使用时需要注意的地方呢?本文一一道来。

常用遍历方式

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机制,最后给出此机制的解决办法,希望能对读者有所启发。