List迭代过滤操作注意点

今天在写一段很简单的代码,本来以为肯定没什么问题,然后直接跑的时候,吆,简单的一个List的操作报错了。仔细一看代码,确实有问题,但是一般真的是如果稍微不小心就会犯下面这种愚蠢的操作。
这里我把代码贴出来:
public static void main(String[] args)
	{
		List<Integer> list = new ArrayList<>(1);
		list.add(1);
		for (Integer a : list)
		{
			if (a == 1)
			{
				list.remove(a);
			}
		}
		list.forEach(System.out::println);
	}
上面的代码报错,我贴错误出来:



然后第一时间觉得有问题,这种遍历然后删除的操作应该要使用迭代器。然后我修改后改成了下面代码:
public static void main(String[] args)
	{
		List<Integer> list = new ArrayList<>(1);
		list.add(1);
		Iterator<Integer> iterator = list.iterator();
		while (iterator.hasNext())
		{
			Integer next = iterator.next();
			if (next == 1)
			{
				list.remove(next);
			}
		}
		list.forEach(System.out::println);
	}

结果一运行同样报错,哎吆,一不小心还是直接去删除List了,然后再次修改才没了问题。最后修改的代码如下:
public static void main(String[] args)
	{
		List<Integer> list = new ArrayList<>();
		list.add(1);
		Iterator<Integer> iterator = list.iterator();
		while (iterator.hasNext())
		{
			Integer next = iterator.next();
			if (next == 1)
			{
				iterator.remove();
			}
		}
		list.forEach(System.out::println);
	}


所以决心好好的研究下这个List在迭代过程中的删减操作为什么很容易报错。打开前面2次报错的代码异常出现的地方,可以清楚的看见报错的原因。
 
final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
modCount,expectedModCount这2个变量是什么搞不懂,所以还是好好研究下吧。在这里我们看那段用迭代器遍历然后直接删除list中一个对象的那段代码,我们来研究一下:
打开JDK源码看一下Arraylist的add和remove操作:
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }


public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

 前面我们已经知道,不管是使用fore循环还是说使用迭代器,List内部操作的都是hasnext()和next()方法。这里贴出源码:
private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;


        public boolean hasNext() {
            return cursor != size;
        }


        @SuppressWarnings("unchecked")
        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];
        }


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


            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }


        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }


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

上面的源码人家JDK里面的注释写的已经很清楚了,这里我们来整理一下:
cursor:表示下一个要访问的元素的索引,从next()方法的具体实现就可看出
lastRet:表示上一个访问的元素的索引
expectedModCount:表示对ArrayList修改次数的期望值,它的初始值为modCount。
modCount:AbstractList类中的一个成员变量,默认是0。
该值表示对List的修改次数,查看ArrayList的add()和remove()方法就可以发现,每次调用add()方法或者remove()方法就会对modCount进行加1操作。
所以我们在对List做迭代操作的过程中,如果这个时候来添加或者删除这个List,这个时候expectedModCount是原来的初始化时候的modCount值,但是modCount这个时候都自增改变了值了,所以肯定就报错了。具体如下图:


OK,现在这个时候我们已经知道了报错的原因了,那么为什么在直接使用interator迭代器来删除就没问题呢?
我们自己看一下Iterator源码里面的remove()方法,就明白了。
public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();


            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                //下面这行是亮点,重新设值expectedModCount了。
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

好了,现在错误原因已经清楚了,和公司CTO聊了下这个问题,人家的原话是这么说的:

操作List在迭代的时候最好只是单纯的迭代,而不要试图去影响原来的那个List,特别是原来那个List的size长度。如果有需要最好也new一个新的list来处理过滤出来的数据最好。而且如果是单纯的要做查询就用Arraylist,如果要做插入和删除操作,最好用likenList。

我个人觉得说的很好很正确,以后编码时候如果要做过滤一个List这种操作时候,最好新new一个容器来搬数据,不要试图直接操作原来那个List。


原文地址:https://www.cnblogs.com/LinkinPark/p/5232899.html