普通for循环、增强for循环、迭代器对集合的增删操作问题

在遍及集合过程中对集合进行增删操作最好不要做,如果非要做可以利用迭代器并发集合,或者同步代码

单线程模式下直接使用迭代器提供的add/remove方法就行或者首先记录下标遍历完后进行增加/删除,多线程模式下建议使用同步或并发结合。

前言:

       普通for循环遍历集合过程中进行删除,如果进行大量删除会报IndexOutOfBoundsException异常,如果少量删除可以成功删除,但是循环的次数会减少,造成结果不准确。

      增强for循环遍历过程中进行删除,会报ConcurrentModificationException异常,并发修改异常。集合遍历时进行增删操作都需要留意是否会触发ConcurrentModificationException异常

 

以下以LList集合为例

普通for循环对集合增删操作问题

  • 增加操作:能正常添加
	public static void main(String[] args) {
		
		List list = new ArrayList<>();
		list.add("a");
		list.add("baidu");
		list.add("c");
		list.add("d");
		
		for (int i = 0; i < list.size(); i++) {
			if ("baidu".equals(list.get(i))) {
				list.add("com");
			} 
		}
		System.out.println(list);
	}

 

  • 删除操作:有问题
	public static void main(String[] args) {
		
		List list = new ArrayList<>();
		list.add("a");
		list.add("baidu");
		list.add("c");
		list.add("d");
		
		for (int i = 0; i < list.size(); i++) {
			list.remove(i);
		}
		System.out.println(list);
	}

代码的原意思是删除所有元素,但结果如图,还剩两个元素

普通for循环、增强for循环、迭代器对集合的增删操作问题_第1张图片

原因:每当删除一个元素时,集合的size方法的值都会减小1,这将直接导致集合中元素的索引重新排序,进一步说,就是剩余所有元素的索引值都减1,而for循环语句的局部变量i仍然在递增,这将导致删除操作发生跳跃。从而导致上述还剩两个元素。

应将循环中的代码修改如下

普通for循环、增强for循环、迭代器对集合的增删操作问题_第2张图片

 

增强for循环对集合的增删操作问题

  • 增加操作:并发修改异常
	public static void main(String[] args) {
		
		List list = new ArrayList<>();
		list.add("a");
		list.add("baidu");
		list.add("c");
		list.add("d");
		
		for (String s : list) {
			if ("baidu".equals(s)) {
				list.add("com");//ConcurrentModificationException
			} 
		}
		System.out.println(list);
	}

普通for循环、增强for循环、迭代器对集合的增删操作问题_第3张图片

使用javap -c 命令查看class文件的字节码

普通for循环、增强for循环、迭代器对集合的增删操作问题_第4张图片

由上图红框圈起的部分不难发现,foreach 循环内部实际是通过 Iterator 实现的,以上代码等同于:

	public static void main(String[] args) {
		
		List list = new ArrayList<>();
		list.add("a");
		list.add("baidu");
		list.add("c");
		list.add("d");
		
		for (Iterator i = list.iterator(); i.hasNext(); ) {
		    String item = i.next();
		    if ("baidu".equals(item)) {
				list.add("com");
			} 
		}
		System.out.println(list);
	}
  • 删除操作:并发修改异常,原理同增加操作一样
	public static void main(String[] args) {
		
		List list = new ArrayList<>();
		list.add("a");
		list.add("baidu");
		list.add("c");
		list.add("d");

		for (String s : list) {
			if ("baidu".equals(s)) {
				list.remove(s);
			} 
		}
		System.out.println(list);
	}

 

迭代器对集合的增删操作问题

  • 增加操作因为Iterator没有add方法,此处我们使用List特有的迭代器ListIterator
  • 正常添加

普通for循环、增强for循环、迭代器对集合的增删操作问题_第5张图片

	public static void main(String[] args) {
		
		List list = new ArrayList<>();
		list.add("a");
		list.add("baidu");
		list.add("c");
		list.add("d");

		ListIterator it = list.listIterator();
		while(it.hasNext()){
			String str = it.next();
			if ("baidu".equals(str)) {
				it.add("com");
			}
		}

		System.out.println(list);
	}
  • 删除操作正常删除
	public static void main(String[] args) {
		
		List list = new ArrayList<>();
		list.add("a");
		list.add("baidu");
		list.add("c");
		list.add("d");

		Iterator it = list.iterator();
		while(it.hasNext()){
			String str = it.next();
			if ("baidu".equals(str)) {
				it.remove();
			}
		}

		System.out.println(list);
	}

为什么迭代器对集合的增删操作不会出错

原因:迭代器内部还是利用ArrayList的添加删除函数进行操作,只不过操作只有会对相应的指针进行修改(下一个),如果进行了删除操作,集合整体长度变小,指向下一个的指针也会相应减小,所以再次访问下一个时就不会发生错误了。

源码:

terator 接口包含以下几个主要方法:

boolean hasNext();  // 检查是否有下个元素
E next();           // 获取下个元素
void remove();      // 移除当前指向的元素

ArrayList 的内部类 Itr 实现了 Iterator 接口,Itr 共有3个成员变量:

 private class Itr implements Iterator {
    int cursor;                      // 下一次遍历的元素的位置
    int lastRet = -1;                // 前一次返回的元素的位置
    int expectedModCount = modCount;

modCount 是 ArrayList 继承自 AbstractList 的一个变量。在AbstractList的源码注释中,是这样解释这个变量的:

The number of times this list has been structurally modified. Structural modifications are those that change the size of the list.

翻译成中文大意为:modCount 为 list 的结构变化次数,即 list 的元素数量变化次数。

查看 ArrayList 的源码,会发现在每次调用 add()remove() 方法,都会进行 modCount++ 操作。

modCount 意为 list 的结构变化次数,而 expectedModCount 可被视为 Itr 内部记录的集合结构变化次数,该变量的作用如下。在 Itr 内部有一个 checkForComodification 方法,如下所示:

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

当集合的实际结构变化次数 和 Itr 记录的变化次数不相等时,则抛出 ConcurrentModificationException 异常。而在 Itr 的 next() 方法 和 remove() 中都调用了 checkForComodification 方法。

ArrayList 内部 Itr 的 remove 方法的源码:

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet);  // 调用集合的remove()方法
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;    // 更新expectedModCount
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

实际上,调用 Itr 的 remove() 方法移除集合元素时,首先会调用 ArrayList 的 remove() 方法,再对 expectedModCount 进行更新。在下次调用 Itr.next() 方法获取下个元素时,不会出现 expectedModCount != modCount 的情况。

Iterator 为什么要检查集合的结构变化次数?

这其实是为了防止多线程并发修改集合,在一个线程遍历集合的同时,另一个线程同时增删集合元素,将无法保证数据的一致性,集合的遍历过程也将被打乱。采用 modCount 机制,在此情景下及时抛出异常,确保同一时间只会有一个线程修改或遍历集合,也即 fail-fast 策略。

你可能感兴趣的:(Java基础)