问题深究01——为什么不要在foreach循环里进行元素的remove/add操作? ...

打印 上一主题 下一主题

主题 906|帖子 906|积分 2718

不要在foreach循环里进行元素的remove/add操作。 remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁。


  • 正例
  1. List<String> list = new ArrayList<>();
  2. list.add("1");
  3. list.add("2");
  4. Iterator<String> iterator = list.iterator();
  5. while (iterator.hasNext()) {
  6.     String item = iterator.next();
  7.     if (删除元素的条件) {
  8.             iterator.remove();
  9.     }
  10. }
复制代码

  • 反例
  1. List<String> list = new ArrayList<>();
  2. list.add("1");
  3. list.add("2");
  4. for (String item : list) {
  5.     if ("1".equals(item)) {
  6.             list.remove(item);
  7.     }
  8. }
复制代码
说明: 以上代码的执行结果肯定会出乎大家的意料,那么试一下把"1"换成"2",会是同样的结果吗?
上面这一段摘自《Java开发手册-嵩山版》编程规约-集合处理-第14条,最后一行的说明并没有给出答案。因此自己琢磨这验证一下。
代码验证


  • 尝试移除元素"1"
  1. List<String> list = new ArrayList<>();
  2. list.add("1");
  3. list.add("2");
  4. for (String item : list) {
  5.     if ("1".equals(item)) {
  6.             list.remove(item);
  7.     }
  8. }System.out.println(list); //输出[2]
复制代码

  • 尝试移除元素"2"
  1. List<String> list = new ArrayList<>();
  2. list.add("1");
  3. list.add("2");
  4. for (String item : list) {
  5.     if ("2".equals(item)) {
  6.             list.remove(item);
  7.     }
  8. }
  9. System.out.println(list);
复制代码

这里涉及到一个语法糖-增强for循环,从错误日志中可以看出增强for循环遍历list时涉及到ArrayList$Itr。
上述代码经过编译器处理后的class文件进行反编译,得到如下的代码片段
  1. List<String> list = new ArrayList();
  2. list.add("1");
  3. list.add("2");
  4. Iterator iterator = list.iterator();
  5. while(iterator.hasNext()) {
  6.     String item = (String)iterator.next();
  7.     if ("2".equals(item)) {
  8.         list.remove(item);
  9.     }
  10. }
复制代码
问题定位

通过日志定位找到抛异常的代码
  1. final void checkForComodification() {
  2.     if (modCount != expectedModCount)
  3.         throw new ConcurrentModificationException();
  4. }
复制代码
从上述这段代码可以得出当modCount!=expectedModCount导致抛出的异常。
摘录ArrayList$Itr的部分代码
  1. private class Itr implements Iterator<E> {
  2.         int cursor;       // index of next element to return
  3.         int lastRet = -1; // index of last element returned; -1 if no such
  4.         int expectedModCount = modCount;
  5.         Itr() {}
  6.         public boolean hasNext() {
  7.             return cursor != size;
  8.         }
  9.    
  10.         public E next() {
  11.             checkForComodification();
  12.             int i = cursor;
  13.             if (i >= size)
  14.                 throw new NoSuchElementException();
  15.             Object[] elementData = ArrayList.this.elementData;
  16.             if (i >= elementData.length)
  17.                 throw new ConcurrentModificationException();
  18.             cursor = i + 1;
  19.             return (E) elementData[lastRet = i];
  20.         }
  21.         public void remove() {
  22.             if (lastRet < 0)
  23.                 throw new IllegalStateException();
  24.             checkForComodification();
  25.             try {
  26.                 ArrayList.this.remove(lastRet);
  27.                 cursor = lastRet;
  28.                 lastRet = -1;
  29.                 expectedModCount = modCount;
  30.             } catch (IndexOutOfBoundsException ex) {
  31.                 throw new ConcurrentModificationException();
  32.             }
  33.         }
  34.     }
  35. }
复制代码
查看ArrayList$Itr可以发现expectedModCount在创建Itr时通过modCount进行赋值,除此之外只有在remove()方法中重新通过modCount进行赋值。
重新查看测试代码后,摘录ArrayList中的相关代码
  1. public Iterator<E> iterator() {
  2.     return new Itr();
  3. }
  4. public boolean remove(Object o) {
  5.     if (o == null) {
  6.         for (int index = 0; index < size; index++)
  7.             if (elementData[index] == null) {
  8.                 fastRemove(index);
  9.                 return true;
  10.         }
  11.     } else {
  12.         for (int index = 0; index < size; index++)
  13.             if (o.equals(elementData[index])) {
  14.                 fastRemove(index);
  15.                 return true;
  16.         }
  17.     }
  18.     return false;
  19. }
  20. private void fastRemove(int index) {
  21.     modCount++;
  22.     int numMoved = size - index - 1;
  23.     if (numMoved > 0)
  24.         System.arraycopy(elementData, index+1, elementData, index, numMoved);
  25.     elementData[--size] = null; // clear to let GC do its work
  26. }
复制代码
通过上述这段代码可以定位到问题了,在调用list.remove()->remove()->fastRemove()中修改了modCount,导致下一次调用next()方法时报异常

分析到这里细心的朋友可能又有一个新的问题了,元素"2"明明已经是list中的最后一个节点了,为什么还会再次调用到next()方法呢?
答案是list.remove()操作后list的size值会减1,而迭代器的hasNext()是通过cursor和size是否相等来判断是否还有下一个元素的。

至此,移除元素"2"报错的原因就分析完毕了。下面开始分析移除元素"1"时,为什么程序没有报异常?
  1. List<String> list = new ArrayList();
  2. list.add("1");
  3. list.add("2");
  4. Iterator iterator = list.iterator();
  5. while(iterator.hasNext()) {
  6.     String item = (String)iterator.next();
  7.     if ("1".equals(item)) {
  8.         list.remove(item);
  9.     }
  10. }
复制代码
可以很容易的发现两段代码的区别在于移除的元素在list中所处的位置不一样。经过对移除元素"2"的代码分析可以得知,问题的症结在于list.remove()方法修改modCount参数。所以我们还是从这个方法开始分析。

通过上述代码可以发现fastRemove()的原理是通过数组拷贝的方式将后一个元素的值拷贝到当前元素所在的位置。拷贝结束后尾节点置null,size-1。
在下一次调用hasNext()时结果为false导致迭代结束。

通过打印元素可以发现,移除元素"1"以后,元素"2"并没有被打印出来。

总结


  • 此问题出现的原因就是调用了list的remove(Object)方法而不是采用迭代器itr.remove()方法进行元素移除。
  • 遍历集合时需要对集合进行增删操作,统一采用迭代器的方式进行。
  • 所有的元素操作都是通过迭代器进行的,因此要进行并发操作时对迭代器加锁是比较合适的一种手段。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

缠丝猫

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表