今天在写一段很简单的代码,本来以为肯定没什么问题,然后直接跑的时候,吆,简单的一个List的操作报错了。仔细一看代码,确实有问题,但是一般真的是如果稍微不小心就会犯下面这种愚蠢的操作。
这里我把代码贴出来:
然后第一时间觉得有问题,这种遍历然后删除的操作应该要使用迭代器。然后我修改后改成了下面代码:
结果一运行同样报错,哎吆,一不小心还是直接去删除List了,然后再次修改才没了问题。最后修改的代码如下:
所以决心好好的研究下这个List在迭代过程中的删减操作为什么很容易报错。打开前面2次报错的代码异常出现的地方,可以清楚的看见报错的原因。
打开JDK源码看一下Arraylist的add和remove操作:
前面我们已经知道,不管是使用fore循环还是说使用迭代器,List内部操作的都是hasnext()和next()方法。这里贴出源码:
上面的源码人家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 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。
风流子弟曾少年,多少老死江湖前。。。