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

news/2024/7/8 1:21:01

目录

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

异常复现

使用迭代器对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进行修改,否则一样会抛出异常。


http://lihuaxi.xjx100.cn/news/903449.html

相关文章

LeetCode-1792. 最大平均通过率【堆,优先队列,贪心】

LeetCode-1792. 最大平均通过率【堆&#xff0c;优先队列&#xff0c;贪心】题目描述&#xff1a;解题思路一&#xff1a;优先队列。首先任何一个班级(x,y)加入一个聪明的学生之后增加的通过率为diff(x1)/(y1)-x/y。那么对p进行堆排序&#xff0c;每次取最大的即可。解题思路二…

19 pandas 分层索引与计算

文章目录分层设置与查询数据index 为有序index 为无序(中文&#xff09;查看数据示例多层索引的创建方式&#xff08;行&#xff09;1、from_arrays 方法2、from_tuples 方法3、from_product 方法多层索引的创建方式&#xff08;列&#xff09;分层索引计算MultiIndex 参数表分…

Linux下的Jenkins安装教程

当前环境 CentOS 7.8Java 11&#xff08;注意当前jenkins支持的Java版本最低为Java11&#xff09;FinalShell 3.9&#xff08;操作环境&#xff09; 安装Jenkins PS&#xff1a;不建议使用Docker安装Jenkins&#xff0c;因为使用Jenkins的时候一般会调用外部程序&#xff0c;…

RabbitMQ 入门到应用 ( 五 ) 应用

6.更多应用 6.1.AmqpAdmin 工具类 可以通过Spring的Autowired 注入 AmqpAdmin 工具类 , 通过这个工具类创建 队列, 交换机及绑定 import org.springframework.amqp.core.AmqpAdmin; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.Di…

华为OD机试 - 静态扫描最优成本(JS)

静态扫描最优成本 题目 静态扫描快速识别源代码的缺陷,静态扫描的结果以扫描报告作为输出: 文件扫描的成本和文件大小相关,如果文件大小为 N ,则扫描成本为 N 个金币扫描报告的缓存成本和文件大小无关,每缓存一个报告需要 M 个金币扫描报告缓存后,后继再碰到该文件则不…

linux018之安装mysql

linux上安装mysql&#xff1a; 第一步&#xff1a;查看是否已经安装mariadb&#xff0c;mariadb是mysql数据库的分支&#xff0c;mariadb和mysql一起安装会有冲突&#xff0c;所以需要卸载掉。 yum list installed | grep mariadb &#xff1a;查看是否安装mariadb&#xff0c;…

适合初学者的超详细实用调试技巧(上)

我们日常写代码的时候&#xff0c;常常会遇到bug的情况&#xff0c;这个时候像我这样的初学者就会像无头苍蝇一样这里改改那里删删&#xff0c;为了根除这种情况&#xff0c;我最近系统学习了调试的技巧&#xff0c;我想要十分详细地讲解&#xff0c;所以大概不会一篇文章写完。…

169、【动态规划】leetcode ——123. 买卖股票的最佳时机 III:二维数组+一维数组 (C++版本)

题目描述 原题链接&#xff1a;123. 买卖股票的最佳时机 III 解题思路 &#xff08;1&#xff09;二维dp数组 动态规划五步曲&#xff1a; &#xff08;1&#xff09;dp数组含义&#xff1a; dp[i][0]&#xff0c;表示无操作。主要由四个状态来表示四种操作。dp[i][1]&…