使用迭代器遍历List抛出ConcurrentModificationException异常分析。

目录

  • 异常复现
  • 原因分析
    • 例子
  • 源码分析
  • 解决方案

异常复现

使用迭代器对java中List遍历时,程序抛出了ConcurrentModificationException异常。这是由于Java的 fast-fail 机制(快速失败)导致的,可以提前预料遍历失败情况。看下面的例子。

    public static void main(String[] args) {
        ArrayList list = new ArrayList<String>(){{
            this.add("1");
            this.add("2");
            this.add("3");
            this.add("4");

        }};

        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            Object o =  iterator.next();
            if("2".equals(o)){
            	list.remove(o);//异常关键
            }
        }
    }

异常发生的关键是在使用迭代器遍历过程中,调用list的remove或者add方法,对所遍历的对象进行了修改。


原因分析

例子

首先看一个简单for循环例子,如果没有java的fast-fail机制,到底会出现什么问题。我们遍历list集合,移除其中的"2"元素。

public static void main(String[] args) {
        ArrayList list = new ArrayList<String>(){{
            this.add("1");
            this.add("2");
            this.add("2");
            this.add("1");

        }};

        for (int i = 0; i < list.size(); i++) {
            Object o = list.get(i);
            System.out.println("遍历到"+o);
            if("2".equals(o)){
                list.remove(o);
            }
        }
        System.out.println(list);
}

程序输出:
遍历到1
遍历到2
遍历到1
[1, 2, 1]

可见,list中没有移除所有的"2"元素。这个残余的"2"其实是第二个"2"。为什么会出现这种问题,原因很简单。在遍历到第一个"2"时,移除了这个元素,为填补这个空缺,后面的元素要向前移动。这时,第二个”2“元素移动到了第一个”2“的位置。在下一躺循环,访问的是元素‘1’,跨过了第二个”2“。

实际上,我们对一个集合遍历时,如果这个集合删除或者增加 了元素,都会对遍历造成影响。

  • 遍历到某个位置,如果在这个位置或者位置之前增加元素,造成当前元素多访问一边。
  • 遍历到某个位置,如果删除这个位置或者位置之前的元素,会漏掉对下个元素的访问。

所以在循环一个集合时,尽量不要增加或者删除这个集合中的元素。

我们再回到刚才的异常。

    public static void main(String[] args) {
        ArrayList list = new ArrayList<String>(){{
            this.add("1");
            this.add("2");
            this.add("3");
            this.add("4");

        }};

        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            Object o =  iterator.next();
            if("2".equals(o)){
            	list.remove(o);//异常关键
            }
        }
    }

为了避免产生上面例子中的错误,使用迭代器iterator对List进行遍历的时候,java是不允许我们直接调用List.remove或者List.add方法对集合进行修改的,否则会抛出ConcurrentModificationException异常。

源码分析

但是Java是如何实现这种检测机制的呢,看下面源码。
在ArrayList的父类AbstractList中,成员变量modCount 记录对集合的修改次数。调用ArrayList中add或者remove方法时,都会使modCount +1;

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
...
	protected transient int modCount = 0;//记录对集合的修改次数
...
}
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
        
	public boolean add(E e) {
       ...
       modCount++;
       ...
    }
    public E remove(int index) {
       ...
       modCount++;
       ...
    }
}

每一次获取ArrayList的迭代器时,会在迭代器对象中用expectedModCount保存此时的ArrayList修改次数。使用Iterator.next方法获取下一个元素时,首先检查modCount、 expectedModCount是否还相等,如果不相等(ArrayList已经被修改),抛出ConcurrentModificationException异常。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
        
		public Iterator<E> iterator() {
       		 return new Itr();
    	}

		private class Itr implements Iterator<E> {
	        ...
	        int expectedModCount = modCount; //保存当前ArrayList修改次数。
			...
	        @SuppressWarnings("unchecked")
	        public E next() {
	            checkForComodification();
	            ...
	        }
			...
	        final void checkForComodification() {
	            if (modCount != expectedModCount)
	                throw new ConcurrentModificationException();
	        }
    }
}

解决方案

使用迭代器、foreach循环遍历时,尽量不要直接调用ArrayList中的add或者remove。java在Iterator迭代器中提供了remove方法,移除ArrayList中的元素。像下面这样。

 public static void main(String[] args) {
        ArrayList list = new ArrayList<String>(){{
            this.add("1");
            this.add("2");
            this.add("2");
            this.add("1");

        }};

        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            Object o =  iterator.next();
            if("2".equals(o))
            {
                iterator.remove();
            }
            System.out.println(o);
        }
        System.out.println(list);
    }
测试结果:
1
2
2
1
[1, 1]

为什么调用迭代器中的remove方法就不会抛出异常的呢,我们看下源码。

private class Itr implements Iterator<E> {
        int cursor;       // 下一次要返回的元素索引
        int lastRet = -1; // 最后一次返回的元素索引
        int expectedModCount = modCount; 
		...
		/**
			调用next方法,主要是返回当前cursor所指向的元素,
			然后让lastRet +1指向这个元素.
		**/
		 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];
        }

		/*
			为了避免遍历过程中移除元素造成漏掉一些元素。
			在移除元素后要对cursor、lastRet 做后移操作。下一次循环还访问当前位置的元素
			(当前位置元素已经被移除,新元素占当前位置)
			并且要更新迭代器中记录的ArrayList修改次数。
*/
        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;  //cursor 后退
                lastRet = -1;//lastRet 后退1
                expectedModCount = modCount; //更新迭代器中记录的ArrayList修改次数。
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
  }

迭代器中有两个变量 cursor,记录下一次要返回的元素索引,lastRet 记录最后一次返回的元素索引。调用next()方法时,返回cursor指向的元素,然后cursor和lastRet都加一。如果在循环中调用了Iterator.remove方法,会让cursor、lastRet都都退一位,避免遍历漏掉元素。
ArrayList不是线程安全的。单线程中,使用迭代器遍历时,我们避免了直接调用ArryList的add、remove方法。也应考虑到多线程时,某个线程迭代器遍历ArryList时,避免其他线程直接对ArrayList进行修改,否则一样会抛出异常。

你可能感兴趣的:(Java,list,java)